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
This seems really nice. The only thing I'm a little uncomfortable with is how much you need to know about the order of the args. I'd probably try to make it work like this:
ReplyDeletedelegate_reader(:shipping, :first_name, :last_name).to(:shipping_address)
Where delegate_reader, delegate_writer, delegate_accessor all do the obvious thing.
Or something like that.
WDYT?
Actually, since you're not always going to want a prefix, perhaps:
ReplyDeletedelegate_reader(:first_name, :last_name).with_prefix(:shipping).to(:shipping_address)
I'll toy w/ this and follow up if I come up w/ something that works.
David,
ReplyDeleteLooks good. I'm a fan of Fluent Interface. We went with that I posted since we were so used to def_delegator and def_delegators that this wasn't much of a leap.
If I were starting from scratch I'd probably look for something more descriptive like what you are suggesting.
I don't think this will work in your particular case, but Rails also has the delegate :to method. Usage looks something like:
ReplyDeleteclass SummaryPresenter
delegate :first_name :last_name, :zip, :to => :shipping_address
There's some docs at http://dev.rubyonrails.org/ticket/4133
Like I said it probably doesn't work for you because of your prefixes...but it's of course very useful, and is mixed into Object I think, so it's available to all classes in the Rails env.
Why do you not go for delegate :to? I discovered your Forwardable post the other day, and almost immediately found another post by somebody who had started using Forwardable after reading your post, but switched to delegate :to. I haven't used either one, so I'm not certain what the tradeoffs are.
ReplyDeletedelegate :foo, :to => :bar isn't much different than def_delegator :bar, :foo. Delegate from Rails also isn't documented so I'm weary of using parts of the framework that aren't documented since they may be 'hidden' for a reason.
ReplyDeleteAlso, I write a lot of code that isn't part of Rails apps so I don't always have the choice.
If I only wrote rails apps, I'd probably go for delegate . :to .. since it's a bit easier to read.
Based on David's suggestion, I implemented a fluent interface for delegation which includes the extra features Jay added on top of Forwardable.
ReplyDeletehttp://brynary.com/2007/4/8/fluent-interface-for-ruby-delegation