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.