Polish Your Gems
As Ruby developers, we often forget how good we have it. We’ve got a awesome library distribution system in Rubygems, we use a powerful and flexible language that just begs to be used for DSLs and a culture of open development and community improvement. In this article, I’ll be talking about the ways you can add that bit of extra polish to your useful new gem. We’ll look at some common usage patterns and how they’re achieved in popular gems.
None of these tips provide any extra functionality for your gem – but they do provide a good user experience. If you have ambitions for your gem to become popular, UI matters and code is a programmers UI.
The Config Block
If you’ve been a rubyist for even a short while, you’ve seen this. The method I’m going to show use is useful for global set up of the gem. Let’s take a look:
#Usage: # Awesome.configure do |a| # a.magic_number = 3 # endmodule Awesome class << self attr_accessor :configuration def config self.configuration ||= Configuration.new end
def configure yield(config) end end
# The configuration object. class Configuration < Hashie::Mash endend
So, there’s a few things to notice here. Firstly, I’ve chosen to have the Configuration object inherit from the fantastic hashie mash gem. This allows us to treat our Configuration object as a hash internally, but means we can expose nice dot syntax in the block itself. In the example a.magic_number = 3 results in the equivalent of a[:magic_number] = 3. Secondly, the config method acts just like the instance method that you’d write if you were making a singleton class, so we don’t have to worry about multiple instances of configuration turning up.
Chain Reaction
If you’re writing a gem which performs some kind of searching on an interface, then you’re presented with somewhat of a API challenge: How can I let my users create complex searches while avoiding a bad case of the hashes? To illustrate what I mean, let’s take a look at Active Record 2 vs 3:
##AR 2Person.find(:all, :conditions => {:name => "Joe", :is_admin => true}, :limit => 10) ##=> Array #AR 3Person.where(:name => 'joe', :is_admin => true).limit(10) ##=> ActiveRecord::RelationIn the Active Record 3 example, method chaining is used instead of a potentially massive hash to define our query. This also has the added benefit of allowing us to define our query easily over time and only execute it when we’re sure we need it by calling a method that sparks off the actual query execution – each for example.
Let’s build a really simple method chaining search object:
class Search class << self def where(args) Search.new(args) end end def initialize(args) @search_arguments = args end def where(args) @search_arguments.merge!(args) self end def each results.each{|a| yield(a)} end def results #Go and get some results! endend
#irb(main):084:0> Search.where(:foo => 'bar').where(:magic_number => 3) #=> #<Search:0x10aa5e9d8 @search_arguments={:foo=>"bar", :magic_number=>3}>
As you can see, we’ve made a pretty convincing search interface here that feels a fair deal like Active Records 3′s query syntax. All you’ll need to do is return self at the end of chainable methods to get the effect. Notice that each is a nice extra here, as it removes the need for an explicit call to results.
Include Everything, Where Appropriate.
As I’m sure you know, you can include a module to add the module’s instance methods to a class or you can extend a module to add class methods. Often, when writing gems, we need to do both. Adding a few lines to the documentation to explain to users that we need to include Library::InstanceMethods and extend Library::ClassMethods is one approach, but that is one extra thing for the user to worry about and there’s no reason the user experience can’t be improved.
HTTParty is perhaps the most used example of this pattern.
Default HTTParty usage looks something like this:
class Thingy include HTTPartyendThe class Thingy will now have the very handy instance method post for POST requests, but will also have the option to call post on the class should your application not actually need to create new Thingys but just to use one.
How does this work? Let’s look at the source – copied from github with some bits chopped out for clarity:
module HTTParty def self.included(base) base.extend ClassMethods #snip base.instance_variable_set("@default_options", {}) endendself.included is a method defined in the Ruby core Module class which get called on when a module is included in another class. base, in this example, is the class Thingy. By overriding it, we’re now able to extend Thingy with HTTPart::ClassMethods saving the user an additional extend.
Also, because we’ve got a handle on Thingy we’re able to perform some setup on the object – as in the example here we’re some default values are set.
The included method, and it’s counterpart extended, are really powerful – but easily abused. In places where you do not need to include and extend there’s really no need to muddy the water by overriding included just so you can use include to bring in class methods – there be dragons down that road.
Be Helpful
This isn’t a code tip – just something I bumped into this week and can’t recommend enough. As developers, perhaps especially as web developers, we should all be aware that it take x seconds for a user to leave a page if they’re having a bad time. I would suspect that the same holds for programmers when they’re having a bad time setting up a gem.
Friendly exceptions are one way to be helpful. Consider replacing the exception message “No configuration file found” with “Cannot find configuration file: config/awesome.yml” – maybe you could even pop in a url to the documentation there.
Ultimately, the experience of your gem is mostly a function of code quality and usefulness. Even libraries which aren’t very user friendly can find great success in the same way as software which isn’t very user friendly can inexplicably thrive.
I’d be intrested to hear your experiances with popular gems. Is there anything that recently caught your eye as a great bit of programmer centric UI? Is there anything that any of the big gems do that’s annoying? As ever, I’ll be lurking around the comment section or on the Tweet machine – do get in touch.
While (er, whilst) implementing the config pattern, I realized the code could be tightened up just a bit:
class Awesome
class Configuration < Hashie::Mash; end
def self.config
yield self.configuration if block_given?
self.configuration
end
private
def self.configuration
@configuration ||= Configuration.new
end
end
Here is few examples that could explain why.
1.9.3p194 :001 > class A
1.9.3p194 :002?> def self.m
1.9.3p194 :003?> puts 'cool'
1.9.3p194 :004?> end
1.9.3p194 :005?> private
1.9.3p194 :006?> def self.m1
1.9.3p194 :007?> puts 'not cool'
1.9.3p194 :008?> end
1.9.3p194 :009?> end
=> nil
1.9.3p194 :010 > A.m
cool
=> nil
1.9.3p194 :011 > A.m1
not cool
=> nil
Now, better one.
1.9.3p194 :033 > class B
1.9.3p194 :034?> def self.m3
1.9.3p194 :035?> puts 'cool with m3'
1.9.3p194 :036?> end
1.9.3p194 :037?> private_class_method :m3
1.9.3p194 :038?> end
=> B
1.9.3p194 :039 > B.m3
NoMethodError: private method `m3' called for B:Class
from (irb):39
from ~/.rvm/rubies/ruby-1.9.3-p194/bin/irb:16:in `'
Here's an example using method_missing:
https://gist.github.com/3507548
And perhaps more in keeping with the example given, here's an example using OpenStruct from ruby's stdlib:
https://gist.github.com/3507564
These were both done quick and dirty, in a real scenario you probably wouldn't allow setting arbitrary keys, etc.
I don't get involved in programming though I like to keep up to date with what is possible/sitepoint news. I totally lost it in 'O' levels when they covered Boolean Algebra!
However, please get something that looks like rubies in your photograph. I know rubies can come in different colours. How about this one you can use "for educational" purposes from my slide library http://photolibrary.cladonia.co.uk/media/2728f7da-3b98-11e0-a058-530c45d3e19e-ultraviolet-fluorescence-of-rubies
Best wishes