Logo of AppSignal

Menu

Ruby Magic

Ruby's Hidden Gems:
Delegator and Forwardable

Michael Michael Kohl on

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:

1
2
3
4
5
6
7
8
9
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:

1
2
3
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:

1
2
3
4
5
class UserDecorator < SimpleDelegator
  def first_name
    "#{super[0]}."
  end
end
1
2
3
4
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

1
2
3
4
5
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:

1
2
3
4
5
  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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 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,

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

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

1
2
3
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:

1
2
3
4
5
6
7
8
9
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:

1
2
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
user.delegate_missing_methods

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

1
2
3
4
5
6
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:

1
2
3
4
5
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.

Guest Author Michael Kohl’s love affair with Ruby started around 2003. He also enjoys writing and speaking about the language and co-organizes Bangkok.rb and RubyConf Thailand.

Latest Ruby Magic articles (see all)

10 latest articles

Go back
Ruby magic icon

Subscribe to

Ruby Magic

Magicians never share their secrets. But we do. Sign up for our Ruby Magic email series and receive deep insights about garbage collection, memory allocation, concurrency and much more.

We'd like to set cookies, read why.