These days I'm working on a project that aggregates a large amount of data on almost every screen. To increase testability of the application we introduced the Presenter pattern. I've previously written an entry introducing the Presenter pattern and a follow up with an additional Presenter example.
In practice our presenters almost always use Forwardable. For example a summary page could display shipping and billing information.
class SummaryPresenterNow, I love forwardable, but it's pretty clear that I can come up with a better solution that will allow me to clean up the def_delegator code.
def_delegator :shipping_address, :first_name, :shipping_first_name
def_delegator :shipping_address, :last_name, :shipping_last_name
def_delegator :shipping_address, :first_name=, :shipping_first_name=
def_delegator :shipping_address, :last_name=, :shipping_last_name=
...
def_delegator :billing_address, :first_name, :billing_first_name
def_delegator :billing_address, :last_name, :billing_last_name
def_delegator :billing_address, :first_name=, :billing_first_name=
def_delegator :billing_address, :last_name=, :billing_last_name=
...
end
We decided to create another method,
delegate_to, which looks very similar to def_delegators, but also takes a hash that allows you to specify a prefix. After making this change the code can be cleaned up to the following.class SummaryPresenterThis was a good first step, but there is obviously more we can clean up.
delegate_to :shipping_address, :first_name, :last_name, ..., :prefix => 'shipping'
delegate_to :shipping_address, :first_name=, :last_name=, ..., :prefix => 'shipping'
delegate_to :billing_address, :first_name, :last_name, ..., :prefix => 'billing'
delegate_to :billing_address, :first_name=, :last_name=, ..., :prefix => 'billing'
end
We also added the ability to create writers by simply adding
:writer => true to the options hash. Following this change the code is concise but just as readable as the original version, if not more because of the noise reduction.class SummaryPresenterBelow is the full code required to add this functionality.
delegate_to :shipping_address, :first_name, :last_name, ...,
:prefix => 'shipping', :writer => true
delegate_to :billing_address, :first_name, :last_name, ...,
:prefix => 'billing', :writer => true
end
module ForwardableExtensionAnd, of course, the tests.
def delegate_to(object, *args)
options = args.last.is_a?(Hash) ? args.pop : {}
args.each do |element|
def_delegator object, element, name_with_prefix(element, options).to_sym
if options[:writer]
def_delegator object, :"#{element}=", :"#{name_with_prefix(element, options)}="
end
end
end
protected
def name_with_prefix(element, options)
"#{options[:prefix] + "_" unless options[:prefix].nil? }#{element}"
end
end
Forwardable.send :include, ForwardableExtension
require File.dirname(__FILE__) + '/../unit_test_helper'
class ForwardableExtensionTest < Test::Unit::TestCase
class Bar
attr_accessor :cat, :dog, :monkey
end
class Foo
extend Forwardable
delegate_to :bar, :cat, :dog, :prefix => 'baz', :writer => true
delegate_to :bar, :monkey
def bar
@bar ||= Bar.new
end
end
test "getter and setter are delegated correctly" do
f = Foo.new
f.baz_cat = "cat"
assert_equal "cat", f.baz_cat
f = Foo.new
f.baz_dog = "dog"
assert_equal "dog", f.baz_dog
end
test "setter is created only when writer is true" do
f = Foo.new
assert_raises NoMethodError do
f.monkey = "raise"
end
end
end