ruby

Ruby's Hidden Gems -Delegator and Forwardable

Michael Kohl

Michael Kohl on

Ruby's Hidden Gems -Delegator and Forwardable

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:

ruby
require 'delegate' User = Struct.new(:first_name, :last_name) class UserDecorator < SimpleDelegator def full_name "#{first_name} #{last_name}" end end

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:

ruby
decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe"

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:

ruby
class UserDecorator < SimpleDelegator def first_name "#{super[0]}." end end
ruby
decorated_user.first_name #=> "J." decorated_user.full_name #=> "J. Doe"

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:

ruby
class MyDelegator < Delegator attr_accessor :wrapped alias_method :__getobj__, :wrapped def initialize(obj) @wrapped = obj end end class UserDecorator < MyDelegator def full_name "#{first_name} #{last_name}" end end

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:

ruby
UserDecorator.superclass #=> MyDelegator < Delegator decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe"

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:

ruby
class MyClass < DelegateClass(ClassToDelegateTo) def initialize super(obj_of_ClassToDelegateTo) end end

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:

ruby
class Logfile < DelegateClass(File) MODE = File::WRONLY|File::CREAT|File::APPEND def initialize(basename, logdir = '/var/log') # Create logfile in location specified by logdir path = File.join(logdir, basename) logfile = File.open(path, MODE, 0644) # This will call Delegator's initialize method, so below this point # we can call any method from File on our Logfile instances. super(logfile) end end

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.

ruby
require 'forwardable' User = Struct.new(:first_name, :last_name) class UserDecorator extend Forwardable def_delegators :@user, :first_name, :last_name def initialize(user) @user = user end def full_name "#{first_name} #{last_name}" end end decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe"

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:

ruby
class UserDecorator extend Forwardable def_delegator :@user, :first_name, :personal_name def_delegator :@user, :last_name, :family_name def initialize(user) @user = user end def full_name "#{personal_name} #{family_name}" end end

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:

ruby
decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.first_name #=> NoMethodError: undefined method `first_name' for #<UserDecorator:0x000000010f995cb8> decorated_user.personal_name #=> "John"

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:

ruby
# In a real Rails app this would most likely be a subclass of ApplicationRecord User = Struct.new(:first_name, :last_name) class UserDecorator attr_reader :user delegate :first_name, :last_name, to: :user def initialize(user) @user = user end def full_name "#{first_name} #{last_name}" end end decorated_user = UserDecorator.new(User.new("John", "Doe")) decorated_user.full_name #=> "John Doe"

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,

ruby
delegate :first_name, :last_name, to: :user, prefix: true

will generate user_first_name and user_last_name methods. Alternatively we can provide a custom prefix:

shell
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:

ruby
decorated_user = UserDecorator.new(nil) decorated_user.first_name #=> Module::DelegationError: UserDecorator#first_name delegated to @user.first_name, but @user is nil

However, with the :allow_nil option, this call will succeed and return nil instead:

ruby
class UserDecorator delegate :first_name, :last_name, to: :user, allow_nil: true ... end decorated_user = UserDecorator.new(nil) decorated_user.first_name #=> nil

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:

ruby
UserDecorator.instance_method(:full_name).bind(user).call #=> "John Doe"

The most interesting aspect of this is that developers can add behavior to objects, without changing their superclass hierarchies.

ruby
require 'casting' User = Struct.new(:first_name, :last_name) module UserDecorator def full_name "#{first_name} #{last_name}" end end user = User.new("John", "Doe") user.extend(Casting::Client) user.delegate(:full_name, UserDecorator)

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:

ruby
user.delegate_missing_methods

To add behavior for the duration of a single block, we can then use Casting's delegating class method:

ruby
Casting.delegating(user => UserDecorator) do user.full_name #=> "John Doe" end user.full_name #NoMethodError: undefined method `full_name' for #<struct User first_name="John", last_name="Doe">

Alternatively, we can add behavior until we explicitly call uncast again:

ruby
user.cast_as(UserDecorator) user.full_name #=> "John Doe" user.uncast NoMethodError: undefined method `full_name' for #<struct User first_name="John", last_name="Doe">

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.

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps