Utilizing Ruby’s Delegate Method

Working with the Principle of Least Knowledge

In Ruby 📖’s standard library, we have the Forwardable module. It provides two methods def_delegator and def_delegators. In Ruby on Rails 📖’s ActiveSupport gem, there is delegate; an analog to the Forwardable methods but perhaps a more intuitive interface and extended interface.

But before we get into delegate, we need to know the laws.

Obey the Law (of Demeter)

The Law of Demeter, or principle of least knowledge, is about constructing your code to have the least knowledge necessary.

The idea is to avoid the “nosy neighbor syndrome”; where I ask my neighbor to ask their neighbor to ask their neighbor about some important thing. Who knows the source of that information. You’ve perhaps played the classic Game of Telephone and seen how the original message morphs as it works from originator to final receiver.

Imagine you wanted to know a person’s mayor. Here’s a quick possible method chain that violates the Law of Demeter: person.address.city.mayor.

The more you chain methods, the more you’re likely to encounter problems.

A Potpourri of Examples

In this example, I’m assuming we have loaded ActiveSupport and have access to Ruby on Rails’s delegate method.

gem 'activesupport'
require "active_support/core_ext/module/delegation.rb"

class Person
  attr_accessor :name
  def address
    Address.new
  end
  delegate :city, :city_mayor, to: :address
end

class Address
  def city
    City.new
  end
  delegate :mayor, to: :city, prefix: true
end

class City
  def mayor
    Mayor.new
  end
end

class Mayor
  def name
    "Ms. Mayor"
  end
end

In the above, I could now use the following to get a person’s mayor:

  • person.address.city.mayor
  • person.city.mayor
  • person.city_mayor

Excuses, Excuses, Excuses

How is delegate not violating the Law of Demeter? In part because we’re declaring the delegation for all instances of Person. They are all are assumed to have a city (via their address).

Put another way, when we instantiate a Person object, it has a city method (and responds_to? :city).

More importantly, what the delegate “macro” does is helps us more clearly delineate what is relevant to the given object’s responsibilities and interface.

Imagine a case where you were writing a test that used the Person data structure for processing or reporting. Let’s also say that creating/instantiating the Address, City, and Mayor was expensive.

And all you want to do is test the following “given a person, when I render their information on the page, I see their mayor”. In this case, you could leverage a test double of the person as follows: person = double(Person, city_mayor: Mayor.new). This is an RSpec 📖 test double syntax. The double of a class will enforce that you are setting

For that test, the person object will use the “mocked” city_mayor for it’s value.

Is It Still a Method Chain?

As I mentioned before, delegate could be considered conceptual method chain. But by using it, you’re saying it’s an expected and acceptable method chain. There are still possibilities of things going wrong, but the parameters of delegate provide some help to minimize the fragility.