In OO design using composition is considered preferable to class inheritance. In the ruby world this is especially true because ruby only allows for single inheritance.
Using composition can lead to some vary ugly looking method chains where an Object is traversing its way through the multiple objects that it is composed of.
For example: from my_object you might need to access property_x that lives nested object two objects away.
$ my_object.other_object.another_object.property_x
You can chain a bunch of calls together and get there. Traversing objects this way is a bit off-putting. Also if you are doing this in one spot you are probably doing it in more than one spot and that is potentially error prone.
Passing a method call from one object to another to do the work is a common thing to want to do so the ruby standard library gives us the Forwardable module. Using Forwardable allows ruby to delegate a method called on ObjectA to another object ObjectB.
A Simple Example
Starting with a From and a To class
class From
attr_reader :to
def initialize
@to = To.new
end
end
class To
attr_accessor :count
def initialize
@count = 0
end
def inc
self.count += 1
end
end
We can create a new From object and get to the count parameter and inc methods through the To object.
$ from = From.new
$ from.to.count
> 0
$ from.to.inc
> 1
$ from.to.count
> 1
It would be better to be able to call from.count and get the count and from.inc to increment the count. This is easily done by extending the Forwardable module in our From class.
require 'forwardable'
class From
extend Forwardable
def_delegators :@to, :count, :inc
end
Now from the console we can do this
$ from.count
> 1
$ from.inc
> 2
$ from.count
> 2
What if instead of count I wanted to call the method total?
That is easy. def_delegators allows us to pass an object and any number of method calls we want to delegate and it uses the existing method name to do it. Using def_delegator we have to do them one method at a time but we have the option of giving methods aliases.
def_delegator :@to, :count, :total
def_delegator :object, :method, :alias=method
def_delegator the :alias parameter is optional. If there is no alias parameter given it defaults to using the given method.
Double Delegating: a more complex example
How about delegating to a nested object? In this example we have a store that goes through a middleman to access the inventory of a warehouse. Standard chaining to get the inventory would look like this:
store.middle_man.warehouse.inventory
Instead of going through the chain and calling inventory it would be better to be able to call something directly on the Store object to get the information. Also calling store.inventory could easily be confusing to someone new coming in to the code. Something like .in_warehouse would be more descriptive of what the value getting returned actually is.
To be able to call .in_warehouse we need to do two delegations.
First from MiddleMan into Warehouse to expose inventory. Second from Store into MiddleMan to access inventory and alias it as in_warehouse.
class Store
attr_accessor :middle_man
extend Forwardable
def initialize
@middle_man = MiddleMan.new
end
def_delegator :@middle_man, :inventory, :in_warehouse
end
class MiddleMan
attr_reader :warehouse
extend Forwardable
def initialize
@warehouse = Warehouse.new
end
def_delegator :@warehouse, :inventory
end
class Warehouse
attr_reader :inventory
def initialize
@inventory = 100
end
end
Bringing it all together
$ store = Store.new
$ store.in_warehouse
> 100