Tuesday, October 28, 2008

Delgation Pattern in Ruby

The Delegation Pattern is a common pattern in all OO programming languages and is generally preferred over the use of inheritance. The beautiful thing about Ruby/RoR is that there are many ways to go about implementing this pattern.

Say we have the following classes:


class User
attr_accessor :first_name, :last_name

def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
end

class Whatever
def initialize(user)
@user = user
end
end

And for whatever reason we want to be able to access the object user's first and last name attributes from the object whatever like the following:


>> user = User.new("Rob", "Zotter")
=> #User:0xb7bf3b98...
>> whatever = Whatever.new(user)
=> #Whatever:0xb7bf1a14...
>> whatever.first_name
=> Robert
>> whatever.last_name
=> Zotter

Here are some ways to go about doing this.

Solution #1 : Wrapper Methods

We could simply add wrapper methods to the Whatever class that simply delegate the calls to the @user object.


class Whatever
def initialize(user)
@user = user
end

def first_name
@user.first_name
end

def last_name
@user.last_name
end
end

As you can see this quickly gets tiring as more and more methods are added to the User class that we would want to delegate. It also bloats the Whatever class.

Solution #2 : Method Missing


class Whatever
def initialize(user)
@user = user
end

def method_missing(method, *args, &block)
if @user.respond_to?(method)
@user.send(method, *args, &block)
else
raise NoMethodError
end
end
end

He were eliminate the need to continuously update Whatever class with wrapper methods when the User class changes by utilizing Kernel#method_missing. Now when the object whatever is sent a message that it does not know it will check the @user object to see if it responds to it and if it does it will delegate that method to it otherwise throwing a NoMethodError. Although this is an improvement over solution #1 it still has its drawbacks. First it is slower since it needs to search the whole Whatever class hierarchy before it reaches method_missing. Second, we do not have control over which methods can be delegated, they simply all get delegated to @user if @user responds to it. Third, whenever using method_missing it is always a little harder for anyone reading your source code to determine exactly what you are trying to do.

Solution #3 : SimpleDelegator


require 'delegate'

class Whatever < SimpleDelegator
def initialize(user)
super(user)
end
end

Here we are using SimpleDelegator which is an implementation of the Delegator interface. We just have to pass in the user to super during instantiation and all methods on user will be available on whatever. This is a little more transparent then using method_missing but by subclassing SimpleDelegator we are preventing ourselves from being able to subclass any other class in the future. We also do not have control over which methods get delegated. Lastly, this way uses inheritance to accomplish composition?!?

Solution #4 : Forwardable


require 'forwardable'

class Whatever
extend Forwardable
def_delegators :@user, :first_name, :last_name

def initialize(user)
@user = user
end
end

Here we are using the Forwardable module. We just have to simply extend our class with the Forwardable module and we explicitly set which methods to delegate using the def_delegators method. As you can see the first argument to that method is the object to which to delegate to and then the following arguments are which methods to delegate to that object.

Finally one last example using ActiveSupport#delegate which you get by default if you are running RoR.

Solution #5 : ActiveSupport Delegate


class Whatever
delegate :first_name, :last_name, :to => :@user

def initialize(user)
@user = user
end
end

This is very similar to the previous example but I prefer this over the former because I think it just reads a little nicer. In this example you list the methods to delegate first then you define what object to delegate to using the :to option. ActiveSupport extends Module with the delegate method so there is no need to extend the Whatever class before using it.

Solutions #4, #5 use metaprogramming techniques to dynamically add class methods to the Whatever class using class_eval which essentially make the class look like it does in solution #1.

As you can see there are many ways to implement the Delegation pattern in Ruby and I'm sure there are plenty more ways that aren't mentioned here. Like the old saying goes theres is more than one way to skin a cat and Ruby certainly gives you more than enough sharp pointy objects to do it with.

1 comment:

pechorin-andrey said...

Thanks, you examples much netter then examples in ruby-api :)