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.

3 Responses to “Fluent interface for Ruby delegation”

  1. # Eric Allam Says:

    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?

  2. # Bryan Helmkamp Says:

    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.

  3. # trans Says:

    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:

    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)

    But I cant say I ever liked those trailing dots. Another means would be to also support a “fluent block” notation, so to speak:

    delegate_reader :line_items do
      to :line_item_list
    end
    delegate_reader :total_value do
      as :subtotal
      to :line_item_list
    end
    delegate_writers :first_name, :last_name do
      to :owner
    end
    delegate_accessors :city, :state, :zip do
      with_prefix :shipping
      to :shipping_address
    end

    T.