In today's exploration of the hidden gems in Ruby's standard library, we're going to look at delegation.
Unfortunately, this term—like so many others—has become somewhat muddled over the years and means different things to different people. According to Wikipedia:
Delegation refers to evaluating a member (property or method) of one object (the receiver) in the context of another original object (the sender). Delegation can be done explicitly, by passing the sending object to the receiving object, which can be done in any object-oriented language; or implicitly, by the member lookup rules of the language, which requires language support for the feature.
However, more often than not, people also use the term to describe an object calling the corresponding method of another object without passing itself as an argument, which can more precisely be referred to as "forwarding".
With that out of the way, we'll use "delegation" to describe both of these patterns for the rest of the article.
Delegator
Let's start our exploration of delegation in Ruby by looking at the standard library's Delegator
class which provides several delegation patterns.
SimpleDelegator
The easiest of these, and the one I've encountered most in the wild, is SimpleDelegator
, which wraps an object provided via the initializer and then delegates all missing methods to it. Let's see this in action:
First, we needed to require 'delegate'
to make SimpleDelegator
available to our code. We also used a Struct
to create a simple User
class with first_name
and last_name
accessors. We then added UserDecorator
which defines a full_name
method combining the individual name parts into a single string. This is where SimpleDelegator
comes into play: since neither first_name
nor last_name
are defined on the current class, they will instead be called on the wrapped object:
SimpleDelegator
also lets us override delegated methods with super
, calling the corresponding method on the wrapped object. We can use this in our example to only show the initial instead of the full first name:
Delegator
While reading the above examples, did you wonder how our UserDecorator
knew which object to delegate to? The answer to that lies in SimpleDelegator
's parent class—Delegator
. This is an abstract base class for defining custom delegation schemes by providing implementations for __getobj__
and __setobj__
to get and set the delegation target respectively. Using this knowledge, we can easily build our own version of SimpleDelegator
for demonstration purposes:
This differs slightly from SimpleDelegator
's real implementation which calls __setobj__
in its initialize
method. Since our custom delegator class has no need for it, we completely left out that method.
This should work exactly like our previous example; and indeed it does:
DelegateMethod
The last delegation pattern Delegate
provides for us is the somewhat oddly named Object.DelegateClass
method. This generates and returns a delegator class for a specific class, which we can then inherit from:
While this may look confusing at first—especially the fact that the right-hand side of inheritance can contain arbitrary Ruby code—it actually follows the patterns we explored previously, i.e. it's similar to inheriting from SimpleDelegator
.
Ruby's standard library uses this feature to define its Tempfile
class which delegates much of its work to the File
class while setting up some special rules regarding storage location and file deletion. We could use the same mechanism to set up a custom Logfile
class like this:
Forwardable
Interestingly enough, Ruby's standard library provides us with another library for delegation in the form of the Forwardable
module and its def_delegator
and def_delegators
methods.
Let's rewrite our original UserDecorator
example with Forwardable
.
The most noticeable difference is that delegation isn't automatically provided via method_missing
, but instead, needs to be explicitly declared for each method we want to forward. This allows us to "hide" any methods of the wrapped object we don't want to expose to our clients, which gives us more control over our public interface and is the main reason I generally prefer Forwardable
over SimpleDelegator
.
Another nice feature of Forwardable
is the ability to rename delegated methods via def_delegator
, which accepts an optional third argument that specifies the desired alias:
The above UserDecorator
only exposes the aliased personal_name
and family_name
methods, while still forwarding to the first_name
and last_name
of the wrapped User
object:
This feature can come in quite handy at times. I've successfully used it in the past for things like migrating code between libraries with similar interfaces but different expectations regarding method names.
Outside the Standard Library
Despite the existing delegation solutions in the standard library, the Ruby community has developed several alternatives over the years and we'll explore two of them next.
delegate
Considering Rails' popularity, its delegate
method may well be the most commonly used form of delegation used by Ruby developers. Here's how we could use it to rewrite our trusty old UserDecorator
:
This is quite similar to Forwardable
, but we don't need to use extend
since delegate
is directly defined on Module
and therefore available in every class or module body (for better or worse, you decide). However, delegate
has a few neat tricks up its sleeve. First, there's the :prefix
option which will prefix the delegated method names with the name of the object we're delegating to. So,
will generate user_first_name
and user_last_name
methods. Alternatively we can provide a custom prefix:
delegate :first_name, :last_name, to: :user, prefix: :account
We can now access the different parts of the user's name as account_first_name
and account_last_name
.
Another interesting option of delegate
is its :allow_nil
option. If the object we delegate to is currently nil
—for example because of an unset ActiveRecord
relation—we would usually end up with a NoMethodError
:
However, with the :allow_nil
option, this call will succeed and return nil
instead:
Casting
The last delegation option we'll be looking at is Jim Gay's Casting
gem, which allows developers to "delegate methods in Ruby and preserve self". This is probably the closest to the strict definition of delegation, as it uses Ruby's dynamic nature to temporarily rebind the receiver of a method call, akin to this:
The most interesting aspect of this is that developers can add behavior to objects, without changing their superclass hierarchies.
Here we extended user
with Casting::Client
, which gives us access to the delegate
method. Alternatively, we could have used include Casting::Client
inside the User
class to give this ability to all instances.
Additionally, Casting
provides options for temporarily adding behaviors for the lifetime of a block or until manually removed again. For this to work, we first need to enable delegation of missing methods:
To add behavior for the duration of a single block, we can then use Casting
's delegating
class method:
Alternatively, we can add behavior until we explicitly call uncast
again:
While slightly more complex than the other presented solutions, Casting
provides a lot of control and Jim demonstrates its various uses and more in his Clean Ruby book.
Summary
Delegation and method forwarding are useful patterns for dividing responsibilities between related objects. In plain Ruby projects, both Delegator
and Forwardable
can be used, whereas Rails code tends to gravitate towards its delegate
method. For maximum control on what is delegated, the Casting
gem is an excellent choice, though it's slightly more complex than the other solutions.