Wednesday, March 19, 2008

Ruby: Replace Temp with Chain

You have methods that can be chained for greater maintainability.

mock = Mock.new
expectation = mock.expects(:a_method_name)
expectation.with("arguments")
expectation.returns([1, :array])

becomes

mock = Mock.new
mock.expects(:a_method_name).with("arguments").returns([1, :array])


Motivation

Calling methods on different lines technically gets the job done, but at times it makes sense to chain method calls together and provide a more fluent interface. In the above examples, assigning an expectation to a local variable is only necessary so that the arguments and return value can be specified. The solution utilizing Method Chaining removes the need for the local variable. Method Chaining can also improve maintainability by providing an interface that allows you to compose code that reads naturally.

Mechanics
  • Return self from methods you wish to allow chaining from
  • Test
  • Remove the local variable and chain the method calls
  • Test
Example

Suppose you were designing a library for creating html elements. This library would likely contain a method that created a select drop down and allowed you to add options to the select. The following code contains the Select class that could enable creating the example html and an example usage of the select class.

class Select
def options
@options ||= []
end

def add_option(arg)
options << arg
end
end

select = Select.new
select.add_option(1999)
select.add_option(2000)
select.add_option(2001)
select.add_option(2002)
select # => #<Select:0x28708 @options=[1999, 2000, 2001, 2002]>

The first step in creating a Method Chained solution is to create a method that creates the Select instance and adds an option.

class Select
def self.with_option(option)
select = self.new
select.options << option
select
end

# ...
end

select = Select.with_option(1999)
select.add_option(2000)
select.add_option(2001)
select.add_option(2002)
select # => #<Select:0x28488 @options=[1999, 2000, 2001, 2002]>

Next, change the method that adds options to return self so that it can be chained.

class Select
# ...

def add_option(arg)
options << arg
self
end
end

select = Select.with_option(1999).add_option(2000).add_option(2001).add_option(2002)
select # => #<Select:0x28578 @options=[1999, 2000, 2001, 2002]>

Finally, rename the add_option method to something that reads more fluently, such as "and".

class Select
def self.with_option(option)
select = self.new
select.options << option
select
end

def options
@options ||= []
end

def and(arg)
options << arg
self
end
end

select = Select.with_option(1999).and(2000).and(2001).and(2002)
select # => #<Select:0x28578 @options=[1999, 2000, 2001, 2002]>

3 comments:

  1. I find that this is one of those places where I wish the ruby parser was a little smarter so I could write:

    Select.with_option(1999)
      .and(2000)
      .and(2001)
      .and(2002)

    rather than:

    Select.with_option(1999) \
      .and(2000) \
      .and(2001) \
      .and(2002)

    Javascript gets it right after all.

    ReplyDelete
  2. Not sure if Object#tap in Ruby 1.9 makes fluent interface implementations any cleaner.

    ReplyDelete
  3. Ruby parser will get it if you end with the dot


    Select.with_option(1999).
    and(2000).
    and(2001).
    and(2002)

    javascript gets it right because it has ; terminator.

    ReplyDelete

Note: Only a member of this blog may post a comment.