If you’ve been writing Object Oriented code for a while (or any code really), you’re probably familiar with expressions such as DRY, the Law of Demeter or Composition over Inheritance. One thing they all have in common is that the can be followed by using delegation (among other solutions).
So, what is delegation? Well, it’s as simple as this, imagine you have three
A calls a method on
B and the only thing
does is call the same (or similar) method on
B is delegating to
We’re going to cover the four most used ways of delegating in Ruby.
The simplest or at least more straight forward way of delegating a method in Ruby, is for the intermediate class to create a regular method just for that.
Here’s an example:
class A def initialize @b = B.new end def start @b.some_method end end class B def initialize @c = C.new end def some_method @c.some_method end end class C def some_method # the method that gets called end end A.new.start
Since doing this, especially when delegating multiple methods, is repetitive and
adds a lot of uninteresting code to your classes, there are alternatives built
into the language. One that is very simple to use, hence the name, is
SimpleDelegator, however it has some drawback, as we’ll see.
SimpleDelegator is used via inheritance, in which the intermediate class
inherits from it. Let’s see how that looks like in our example:
class A def initialize @b = B.new(C.new) end def start @b.some_method end end class B < SimpleDelegator end class C def some_method # the method that gets called end end A.new.start
We’ve made two changes, the first one was removing all the code from the
class and making it inherit from
SimpleDelegator. Because of that we were able
B and pass in an instance of
C. From there, any method we call
B will be called on it if it is defined, or will be delegated to
There lies one of the problems with
SimpleDelegator, by default it delegates
everything, which might not be what you want.
With our refactor we made
A be aware of
C, we don’t want that. We can fix
that by moving that knowledge to the outer scope, or back into
Moving to the outer scope would look like this:
class A def initialize(b) @b = b end ... end ... b = B.new(C.new) A.new(b).start
Moving it back into the initializer takes advantage of a
__setobj__ that takes an argument and uses that object as the
object to whom the methods are delegated.
class A def initialize @b = B.new end ... end class B < SimpleDelegator def initialize __setobj__(C.new) # or super(C.new) end end ...
If for some reason you need access the instance of
C from within
B, you can
A very good use case for
SimpleDelegator in the Real World™ is to implement
the Decorator Pattern. For
example, if you have a base
User class and want to add to it some subscription
functionality, without actually adding it to all instances of the
could do something like this:
class User # user stuff end class UserWithSubscription < SimpleDelegator # code that assumes a user has a subscription end ## when you need to do some subscription ## related things to a user user = UserWithSubscription.new(@user)
To all intents and purposes,
UserWithSubscription behaves just like an
but with some extra functionality. Because of how
SimpleDelegator works, it is
a superset of
DelegateClass is more focused version of
SimpleDelegator. Everything is the
same except that you have to define upfront what is the class of the object to
whom you’ll be delegating. The benefit you reap from this is that it is more
Our example would look very similar:
class A def initialize @b = B.new end def start @b.some_method end end class C def some_method # the method that gets called end end class B < DelegateClass(C) def initialize __setobj__(C.new) end end A.new.start
The main difference here is that the
C class needs to exist when
defined, so that we can use it in the signature.
DelegateClass share the problem of delegating
everything. If you wish to only delegate one or two methods, you’re probably
better off using
Forwardable. It is a
Module you can
extend, in order to
get some delegation functionality.
Let’s go back to our example but delegate only
require "forwardable" class A def initialize @b = B.new end def start @b.some_method end end class B extend Forwardable def_delegator :@c, :some_method def initialize @c = C.new end end class C def some_method # the method that gets called end end A.new.start
Notice that in order to use
Forwardable we need to require it, it does not
come auto loaded. Then, we use the
def_delegator class level method that takes
a symbol with object to whom we want to delegate. It can either be a method, or
an instance variable (as we’ve done here).
In order to define multiple delegator methods at once there is also the
Forwardable works just fine, but it’s API is kind of strange, in my opinion.
Especially because you possibly have to change the method you call when going
from one delegated method to two.
Rails, more precisely
ActiveSupport, has it’s own method for delegation for
pretty much the reasons I mention above. In the original commit
DHH claims that it is because
Forwardable does not support multiple delegations
at once, which looking at the source code from 2001
(way before Rails existed) does not look like it is true, but again, the API for
Forwardable is not great now, and was not great then.
That being say, here’s the last example written in
require "active_support/core_ext/module/delegation" class A def initialize @b = B.new end def start @b.some_method end end class B delegate :some_method, to: :@c def initialize @c = C.new end end class C def some_method # the method that gets called end end A.new.start
I think this looks nicer and easier to read, however, most of the times it’s not
ActiveSupport in you project just for this. If you’re on a Rails
project or a project that has
ActiveSupport already, then by all means use it.
If you’ve enjoyed this Ruby Bit you should really subscribe to our newsletter, where other Ruby Bits and more great articles are shared every week.