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 forBut the current Delegate class doesn't work that way. So, I decided to see how hard it would be to write it up myself.
class Foo
include Delegation
delegate Array, :size, :push, :pop, :<<, :[]
end
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)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.
mod.extend(self)
end
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
Looks good!
ReplyDeleteYou 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.
why not just do:
ReplyDeleteclass Foo
extend Delegation
end