Thursday, April 13, 2006

Ruby Initialization Chain Module

The Ruby Initialization Chain module comes from my current project where I was using the Initialization Chain pattern. The Initialization Chain module allows you to call setter methods that set a value and return the instance. This chaining of method calls allows you to initialize an object with one line of code.
foo = Foo.new.set_bar("baz).set_cat("dog")
If you were to define the setter methods, they would all follow the same basic pattern
def bar=(value)
@bar = value
end

def set_bar(value)
bar=value
end
The Initialization Chain module assumes you will follow this pattern and removes the need to define them.

Code and Tests:
module InitializationChain
alias :pre_init_chain_method_missing :method_missing
def method_missing(sym, *args, &block)
if matches_set_and_writer_exists?(sym)
self.send convert_set_to_setter(sym), *args, &block
return self
end
pre_init_chain_method_missing(sym, *args, &block)
end

def matches_set_and_writer_exists?(sym)
matches_set?(sym) && writer_exists?(sym)
end

def match_regex
/^set_/
end

def convert_set_to_setter(sym)
"#{sym.to_s.sub(match_regex,'')}=".to_sym
end

def writer_exists?(sym)
self.respond_to?(convert_set_to_setter(sym))
end

def matches_set?(sym)
sym.to_s =~ match_regex
end
end

class Foo
include InitializationChain

attr_accessor :bar

def cat
@cat
end

def cat=(value)
@cat=value
end
end

require 'test/unit'

class InitializationChainTest < Test::Unit::TestCase
def test_matches_set
assert(Foo.new.matches_set?(:set_foo))
end

def test_writer_exists
assert(Foo.new.writer_exists?(:bar))
end

def test_add_dynamic_set_for_attr
foo = Foo.new.set_bar("baz")
assert_equal "baz", foo.bar
end

def test_add_dynamic_set_for_defined_setter
foo = Foo.new.set_cat("dog")
assert_equal "dog", foo.cat
end
end

2 comments:

  1. Anonymous7:09 PM

    In Ruby, don't you think

    foo = Foo.new :bar => 'baz', :cat => 'dog'

    would be much more intuitive? You could perhaps implement Foo#update_attributes so you can update an already initialized object. It is easier to read, and Hash initialization is a pretty common pattern in Ruby.

    ReplyDelete
  2. Anonymous10:07 PM

    I'm not sure if I either way is better than the other.

    On one hand I do like explicitly calling the methods I'm concerned with instead of depending on a hash.

    On the other hand I like how options read very explicitly.

    Perhaps a happy medium could be creating a module that uses the parameters from the hash to call the associated methods on the object.

    I'll probably write that up this weekend...

    ReplyDelete

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