Saturday, March 25, 2006

Ruby Delegation Module

Recently while working on a problem I needed an object that acted like an Array, but had additional methods. Since I was working in Ruby I could have added additional methods to the Array class or created a subclass of Array. However for my specific problem it seemed like a better option to create a new class, expose the necessary methods, and delegate their behavior to an instance variable of type Array.

Ruby already has a delegate class in the standard library, but it doesn't have exactly the behavior I was looking for. I hoped that including a Delegation module would allow me to specify which methods I wanted to delegate to with a simple list. This could be achieved by specifying which type of object I want to delegate to and which methods I wanted my object to support.
# This is the behavior I was looking for
class Foo
include Delegation

delegate Array, :size, :push, :pop, :<<, :[]
end
But the current Delegate class doesn't work that way. So, I decided to see how hard it would be to write it up myself.

The delegate method takes a type and one or many methods. The delegate method defines the method __delegate_instance__. The __delegate_instance__ method initializes a new instance of the specified type the first time it is executed, and returns that instance each additional execution. Then, the delegate method loops through each method in the methods argument and defines a method that delegates behavior to the method of the same name on the delegate instance.
def delegate(klass, *methods)
define_method("__delegate_instance__") { @__delegate_instance__ ||= klass.new }
methods.each do |method|
define_method(method.to_s) { |*args| __delegate_instance__.send method.to_sym, *args }
end
end

This is the majority of the Delegation module. The only other necessary behavior is a change to the append_features class method. Because delegate needs to be a method on the class and not an instance method the append_features method needs to be modified to extend the class instead of the instances of the class.
def self.append_features(mod)
mod.extend(self)
end
That's basically it. If the type you wanted to delegate to needed arguments for it's constructor you would need to alter the delegate method to take those arguments.

Full code plus tests:
module Delegation
def delegate(klass, *methods)
define_method("__delegate_instance__") { @__delegate_instance__ ||= klass.new }
methods.each do |method|
define_method(method.to_s) { |*args| __delegate_instance__.send method.to_sym, *args }
end
end

def self.append_features(mod)
mod.extend(self)
end
end

class Foo
include Delegation

delegate Array, :size, :push, :pop, :<<, :[]
end

require 'test/unit'

class DelegationTest < Test::Unit::TestCase
def test_methods_are_added_correctly
foo = Foo.new
foo.push "one"
assert_equal 1, foo.size
foo << "baz"
assert_equal 2, foo.size
assert_equal "baz", foo[1]
end
end

2 comments:

  1. Anonymous12:58 AM

    Looks good!

    You might want to check out the 'forwardable' module in the standard library. I think it does most of what you need. I do like your touch of just mentioning the class you want to delegate to and not having to worry about instantiating it yourself, though.

    ReplyDelete
  2. why not just do:
    class Foo
    extend Delegation
    end

    ReplyDelete

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