April 07, 2007
Fluent interface for Ruby delegation
On our latest project at EastMedia, we’ve been using Ruby’s Forwardable delegation library to help avoid violating the Law of Demeter. Unfortunately, Forwardable’s interface has a few problems:
- It doesn’t read well.
- It requires you to remember the order of the arguments.
- It has no support for automatically prefixing method names.
- It has no support for automatically setting up writers and accessors.
Back in February, Jay Fields wrote an extension for Forwardable that resolved the last two points, but he didn’t try to address the first two. In a comment on that post, David Chelimsky suggested a possible fluent interface for delegation, but to my knowledge no one had built and released it.
Here are some examples of what I came up with:
1 2 3 4 5 6 7 8 |
class Order extend Forwardable delegate_reader(:line_items).to(:line_item_list) delegate_reader(:total_value).as(:subtotal).to(:line_item_list) delegate_writers(:first_name, :last_name).to(:owner) delegate_accessors(:city, :state, :zip).with_prefix(:shipping).to(:shipping_address) end |
The library I wrote wraps Forwardable and also includes support for prefixing and automatic writers and accessors in the style of Ruby’s attr_reader, attr_writer and attr_accessor. Here is the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
require "forwardable" module FluentForwardable class FluentForwarder def initialize(klass, readers, writers, *methods) @klass = klass @readers = readers @writers = writers @methods = methods end def with_prefix(prefix) @prefix = prefix self end def as(custom_name) raise "Cannot delegate multiple methods with a custom name" if @methods.size > 1 @custom_name = custom_name self end def to(receiver) for method in @methods new_method = new_method_name(method) if @readers @klass.class_eval { def_delegator receiver, method, new_method } end if @writers @klass.class_eval { def_delegator receiver, :"#{method}=", :"#{new_method}=" } end end end protected def new_method_name(method) return @custom_name if @custom_name @prefix ? "#{@prefix}_#{method}" : method end end def delegate_reader(*methods) FluentForwarder.new(self, true, false, *methods) end alias_method :delegate_readers, :delegate_reader def delegate_writer(*methods) FluentForwarder.new(self, false, true, *methods) end alias_method :delegate_writers, :delegate_writer def delegate_accessor(*methods) FluentForwarder.new(self, true, true, *methods) end alias_method :delegate_accessors, :delegate_accessor end Forwardable.send :include, FluentForwardable |
I’ve put the specs into a Pastie to save space. The resulting specdoc is:
delegating readers
- should delegate the reader to the receiver
- should not delegate writers
delegate writers
- should delegate the writer to the receiver
- should not delegate readers
delegating accessors
- should delegate readers
- should delegate writers
delegating to a method with a different name
- should use the specified name as the new method name
delegating with a prefix
- should use the prefix for any readers
- should use the prefix for any writers
I couldn’t think of any way to implement this without requiring that the to() segment be last, but I’d love to hear suggestions about how that could be done. Also, as always, I’m interested in any constructive critique of this code and the specs.
April 24, 2007 at 12:56 AM
After working with rspec I am really starting to like fluent interfaces because of the increased readability and possibility for greater flexibility. It reminds me of Higher Order Messaging which I was introduced to recently and haven’t fully digested.
What do you think about turning Active Record into a fluent interface?
April 24, 2007 at 11:18 PM
Eric—Interesting idea. I worry about adding the extra complexity without too much of a gain in readability. Most of ActiveRecord reads quite naturally with its standard interface.
I wasn’t familiar with the concept of Higher Order Messaging. Thanks for that link.
June 20, 2007 at 9:14 PM
Never heard the term “fluent” applied to this before. I’ve always heard it referred to as “magic dot”. But maybe “fluent” is better, but it take the “magic” out ;) One problem with this notation though is when the statements get long. One way to handle that of course is:
But I cant say I ever liked those trailing dots. Another means would be to also support a “fluent block” notation, so to speak:
T.