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