Wednesday, November 3, 2010

The options Hash, a classic ruby pattern

If you use rails, or any of a thousand other ruby gems, you're familiar with using methods like this:


phone = SweetPhone.new
if caller.is_my_friend?
  phone.ring!(:volume=>6,:ringtone=>:smooth_jazz)
elsif caller.is_my_boss?
  phone.ring!(:volume=>11,:ringtone=>:totally_obnoxious)
else
  phone.ring!
end

yes, the conditional could be constructed to just build the hash and only call the method in one place, I know; that's not today's point. What I'm emphasizing is the fact that this "ring" method can just be called by itself, or with any of a series of options. It's nice, because you have intelligent defaults, and easy configuration if necessary.

The problem I see, is that even though this is the simplest of the basic ruby patterns that you see every day, and even though we're all more than happy to use these APIs in third-party libraries, I still run across code every now and then in home-brewed applications that looks like this:


class LamePhone
  def ring!(volume,ringtone,vibrate,max_rings,caller_id)
    #...
    #implementation
    #...
  end
end

and 80% of the code that calls these long-paramed functions uses the SAME SET OF PARAMETERS.

If you've been programming for a while, this isn't news to you, so disregard this post, but if you're new to ruby or software in general, take note: repeating yourself is a bad sign. If you're sending the same message from 50 places in your code, and 40 of them use the same function list, it may be time to set some intelligent defaults. To fix this anti-pattern, the options hash works beautifully:


class SweetPhone
  def ring!(options = {})
    adjust_volume(options[:volume] || 6)
    ringtone = pick_ringtone_from_category(options[:ringtone] || :all)
    sound_the_ringer!(ringtone)
  end
end

5 comments:

Anonymous said...

I prefer a pattern like this, so default values are reusable and not mixed throughout the code.

class MyToolkit
def defaults
@options ||= {
:opt => "value",
:integer => 33,
:type => "awesome"
}
end

def method(opts={})
# one time only option overrides
opts = defaults.merge(opts)
# OR... you can make the settings persist between method calls
defaults.merge!(opts)

# ... now do something useful
end
end

Anonymous said...

In the first paragraph you say: "the conditional could be constructed to just build the hash and only call the method in one place". Can you please show an example?

Ethan Vizitei said...

@first_Anonymous

I like that approach, although I haven't used it myself. The only thing that might make me think for a minute about not using it is that I habitually avoid instance variables if possible, but in this case since the only thing that references it is the defaults method, it's really not a "risky" imposition on an otherwise functional process. I may refactor some of my current code to do it this way to see how I like it, thanks for the comment!

@second_Anonymous

What I mean is that instead of calling "ring!" in each branch of the conditional, you could do something like this:

opts = {:volume=>3}
if caller.is_my_friend?
opts.merge!(:volume=>6,:ringtone=>:smooth_jazz)
elsif caller.is_my_boss?
opts.merge(:volume=>11,:ringtone=>:totally_obnoxious)
end

phone.ring!(opts)

I don't know how much better that is, really, since it requires introducing another local variable, but if you prefer only calling ring in one place rather than inside each conditional branch and the each clause at the end, it's viable.

Robert Pankowecki said...

Rails usually does it this way:

def method(options={})
options.reverse_merge!(:volume => 10)
ring(options)
end

Ethan Vizitei said...

@Robert

I think I like your version best out of what's on the page so far. Defaults are declared in one place, but in context of the method. Those Rails guys know what they're doing. :)