Forwarder
Delegation made readable.
Rational
It seems that Ruby's built in Forwardable module does a decent job
to provide for delegation. However its syntax is more than terse, it is, IMHO,
unreadable. At a certain moment it came to me that relearning the correct
application of def_delegator, frequent usage non withstanding is not
what I want to use my time for.
From that desire Forwarder was created. As Forwardable it is not
intruisive but once a module is extended with it the following methods spring
into live: forward, forward_all and forward_to_self. The
first two replace and enhance the functionality of def_delegator and
def_delegators, while the third is a kind of a alias_method on
steroids.
Parameters
The first parameter, (or paramters in the case of forward_all) is (are)
a symbol(s) or string(s) indicating the message to be forwarded. That is
a message of which the receiver is an instance of the module in which
forward was called.
Ater this we have a hash style parameter which needs a target specification,
indicated by :to, :to_chain or :to_object. It can contain an optional
:as parameter translating the method name and an equally optional :with
paramter allowing us to provide paramters to the forwarded message.
I refer to the :as parameter as the translation and the :with parameter
as the parametrization.
This might be confusing at first, but the follwing examples shall demonstrate how simple things really are.
Examples
The forward Method
Target specified with :to
The :to keyword parameter and can be either a Symbol (or String), thus representing
an instance method or an instance variable of the receiver. It can also be a lambda that
will be evaluated in the receiver's context. If an arbitrary object shall be the receiver of the
message, than the :to keyword can be replaced by the :to_object, and if the target of
the message shall bet the result of chained method calls on the receiver :to_chain is
at your service.
class Employee
extend Forwarder
forward :complaints, to: :boss
end
This design, implementing some wishful thinking that will probably not pass
acceptance tests, will forward the complaints message, sent to an instance
of Employee, to the object returned by this very instance's boss method.
As feared the implementation did not live up to the expectations (hence the desperate need of foraml specifications) and the following adjustment was made, in some desperate hope to fix the bug:
class Employee
extend Forwarder
forward :complaints, to: :boss, as: :suggestions
end
This behavior being clearly preferable to the one implemented before because the
receiver of complaints is still forwarding the call to the result of the
call of its boss method, but to it's suggestions method. (Well that is
not precise wording, but we shall make an abstraction about how the object returned
by boss handles the suggestions message.)
Finally, however, the implementation looked like this
class Boss
extend Forwarder
forward :complaints, to: first_employee
forward :problems, to: first_employee
forward :tasks, to: first_employee
end
However this did not work as no first_employee was defined yet. This seems
a task so simple that a method for this seems almost too much code.
Forwarder let us allow an implementation on itself.
The other thing that catches (or should, at least) the reader's eye is the terrible code repetition.
To get rid of it, we will indulge us by looking ahead to the forward_all method, which of course
is just short for three forward calls with each of its positional parameters.
class Boss
extend Forwarder
forward :first_employee, to: :@employees, as: :[], with: 0
forward_all :complaints, :problems, :tasks, to: :first_employee
end
Here we see the first use case of a parametrization, paired with a translation. Please note that the first does not necessarily imply the second, the following example might be reasonable code.
class Train
extend Forwarder
forward :signal, to: :@signaller, with: {strength: 10, tone: 42}
end
As a side note, I do not enourage the exposure of instance variables as in the
examples above, but it still might make your code shorter, which is an asset
of its own of course. Furthermore it allows a faster transation from Forwardable
if it is used to delegate to instance variables.
The above Boss case was badely written of course as Array#first gives us the
perfect opportunity to get rid of the with: parameter, which is somehow a little
bit of a code smell, I admit. Let us do better:
class Boss
extend Forwarder
forward :first_employee, to: :@employees, as: :first
forward_all :complaints, :problems, :tasks, to: first_employee
end
forward_all
forward_all allows us to forward more then one message to a target. It is a shortcut
for calling forward to each of its method parameters. As one can see in the next
example it supports all kinds of target parameters, :to, :to_chain, but also :to_object
Target :to_chain
The example above is still too verbose. For what we know there is no need to define a
delagation for the first_employee method. And this is the use case where :to_chain
seems the right tool to use, let us see it's application at work:
class Boss
extend Forwarder
forward_all :complaints, :problems, :tasks, to_chain: [:@employees, :first]
end
Now we forward, almost anonymously to @employeees.first without defining a method, or
forwarder to bridge this gap.
As you might guess, the complaints message is sent to the result of sending first
to the @employees instance variable. As (no pun intended) with the to: version
of forward, one can change the message name with the as: parameter, aka translation.
It is uncommon, but not impossible to use a translation in forward_all
class Boss
extend Forwarder
forward_all :complaints, :problems, :tasks,
to_chain: [:@employees, :first],
as: :request
end
Here we go, seems quite a realistic model to me.
Performance
If you are concerned about performance, but you should not yet, I have good news for you. Using
Forwarder will be a performance hit. Now why should that be good news? Well it is good news
for two reasons. Firstly by using Forwarder the performance hit notwithstanding you show that
you are not concerned by premature optimization but much more with clean, concise and readnale
design. Secondly if you run into performance issues and profiling shows that a forward target
is hit frequently, chances are that you found one of your performance bottlenecks. Just implement
the forward manually as a method and you shoud see quite some improvement. Now I am sure
you'd wish that all your performance issues are that easy to fix.
As I said: Two pieces of Good News!
License
This software is licensed under the MIT license, which shall be attached to any deliverable of this software (LICENSE) or can be found here http://www.opensource.org/licenses/MIT