Perusing delegate.rb from Ruby’s Standard Library
It is a rather common claim that Object Oriented programming is a lot about passing messages between objects. Also, OO encourages us to find the right nouns and verbs in order to solve a given problem. I often like to think about a program as a theatrical stage, upon which participants act and speak with each other. Sometimes, though, a character might need to say something to another character through a third participant; that go-between actor would be asked to delegate the message to the target character. Back to programming, delegation
…refers to evaluating a member (property or method) of one object (the receiver) in the context of another, original object (the sender). / Wikipedia
This definition is quite similar to the theatrical analogy — it defines delegation as the process of passing a message to an object only to have that object forward the message to another object. But why would we might want to do that?
I’ve used SimpleDelegator
from Ruby’s standard library in a previous post, and it feels like it’s well worth a dedicated post of its own. First, I will demonstrate how using delegate.rb can help us in designing strong-yet-flexible interfaces. Afterwards, looking at the source code itself, I will try to explain a bit of the magic that happens behind the scenes.
Suggest Me a Movie, Would You?
Let’s assume we are working on the backend part of a movie recommendation engine. In our simplified world, what we currently need to know about a Movie
is the (numeric) rating (score) it got from iMDb and Rotten Tomatoes. Assuming both scores are normalized on the same scale, we would also like to get a single simple number — average_score
— based on the average of the two external scores. It looks like this:
What we need next is a class that (perhaps after some regression modeling) holds an array of Movie
s, called RecommendedMovies
, upon which we can perform relevant queries:
Ha, that’s pretty straightforward. We can pat ourselves on the shoulder for creating a dedicated object (i.e. not sticking to a primitive array) which has a clear and useful interface. Let’s get some data in and take it out for a test drive:
north_by_northwest = Movie.new('North by Northwest', 85, 100)
inception = Movie.new('Inception', 88, 86)
the_dark_knight = Movie.new('The Dark Knight', 90, 94)
recommended_movies = RecommendedMovies.new([north_by_northwest, inception, the_dark_knight])
We can query recommended_movies
in a simple manner:
recommended_movies.best
=> #<Movie:0x007fbcf7048948 @name="North by Northwest", @imdb_score=85, @rotten_tomatoes_score=100>
Limited Responsiveness
Our RecommendedMovies
seems to work as expected, but with one significant drawback: we initiated it with an array, but we lost all original Array
behavior: if we run recommended_movies.count
, we’ll get a NoMethodError
in return. This is somewhat limiting, as it’s very probable that we would benefit from using all those wonderful Array
(and Enumerable
) methods upon RecommendedMovies
! We could implement method_missing
in our class, yet perhaps we could turn to a more elegant solution, found in Ruby’s standard library — delegate.rb.
This library offers two concrete solutions to our problem — both of them are implemented through inheritance. While DelegateClass
is worth a dive-in on it’s own, the simpler of the two is SimpleDelegator
and it suits the case in question very well. We could use it like this:
And… that’s pretty much it. Everything works the same as before, and now we have all the original array methods at hand. Basically, we applied the Decorator Pattern upon an array. Referring to the same data that we’ve prepared earlier, running recommended_movies.count
now simply returns 3
. Notice that I’ve omitted the initialize
method since declaring and referring to the movies
instance variable is no longer needed! It’s as if self
in our RecommendedMovies
instances is the array with which we initiated the RecommendedMovies
object…
Behind The Scenes
The source code for delegate.rb is available here; I will refer to it with line numbers, so you might want to keep it open on the next tab. A stroke on the l
key will facilitate jumping to a specific line. Furthermore, there some interesting commentary in that file that I recommend reading in case you fancy a deeper dive. I will analyze our usage of SimpleDelegator
from the bottom up.
As a result of inheriting from SimpleDelegator
, the ancestors chain for RecommendedMovies
looks like this:
recommended_movies.class.ancestors
=> [RecommendedMovies, SimpleDelegator, Delegator,
#<Module:0x007fed5005fc90>, BasicObject]
That’s quite a change from the initial version’s ancestors chain — [RecommendedMovies, Object, Kernel, BasicObject]
. The reason for the difference is that SimpleDelegator
inherits from another class — Delegator
(line 316) — which in turn inherits from BasicObject
(line 39). This is why Object
and Kernel
are out of the chain now. The unfamiliar#<Module:0x007fed5005fc90>
(which would probably appear slightly different on your machine, if you run this code) is an anonymous module, defined and included by theDelegate
class (line 53); it serves as a chopped-down version of the Kernel
module: Kernel
is duplicated and placed in temporary variable (line 40); then, actions are performed on the variable’s class level (line 41) that un-define several methods from it (line 44, line 50). After these modifications, the updated Kernel
finally gets included in Delegate
. This explains the ancestors chain we’re seeing.
Transparent Initialization
As noted earlier, I’ve omitted the initialize
method from my updated RecommendedMovies
class. Ruby will automatically call initialize
on a new object (i.e. after we called new
on a class), but since I did not implement an initialization method, it went up the ancestors chain to look it up, as expected. SimpleDelegator
doesn’t implement it as well, but Delegator
does (line 71). It is expecting a single argument, obj
, which is the argument with which the RecommendedMovies
instance was created — in our case, an Array
of Movie
objects — and that is the object we will delegate messages to.
Inside, Delegator#initialize
simply calls the __setobj__
method, passing the same obj
as an argument again. But Delegator
does not implement __setobj__
: if it was to receive such call, it would raise an error (line 176). This is because Delegate
serves as an abstract class. Its subclasses should implement __setobj__
themselves, and indeed, SimpleDelegator
does that (line 340). SimpleDelegator#__setobj__
simply stores obj
in an instance variable named delegate_sd_obj
(sd
stands for SimpleDelegator). Remember, in our example, self
is still recommended_movies
!
Delegation!
As demonstrated earlier on, once our recommended_movies
object is born, we can use it as a decorated array. When we call the best
method upon it, Ruby locates it in the object’s class, RecommendedMovies
, and executes it for us. But when we call count
, Ruby won’t find it in our class. The interpreter then traverses through the ancestors chain looking up the method, but alas, count
is not defined in any of our class’ ancestors!
This is the point where method_missing
comes into play. If Ruby finishes the regular method lookup with no findings, it will not throw a NoMethodError
right away; instead, it will commence the lookup again, this time looking for method_missing
. If any of the classes in the ancestors chain define this method, it will be called. If not, we will receive that NoMethodError
after the search reaches its end at the top of the chain.
In our context, the Delegator
class defines method_missing
(line 78). First, it fetches the target object we will try to delegate to by calling __getobj__
(line 80), which is implemented by SimpleDelegator
(line 318). Essentially, this method hands us back the target object stored in @delegate_sd_obj
. It will then try to call the method in question upon the the target object (line 83). If the target object doesn’t respond to the method, Delegate#method_missing
will check if Kernel
does and will call it accordingly (line 85). Otherwise, it will call super
(line 87), which, at this part of the narrative, will result in that NoMethodError
. Quite a journey!
There is some more code in Delegate#method_missing
, but this is the core of action. In his book Metaprogramming Ruby 2, Paolo Perrotta defines a Blank Slate as “a skinny class with a minimal number of methods” (p. 66). The Delegate
class uses this technique by inheriting from BasicObject
which eliminates unwanted surprises, but remember that at the same time, the intelligent implementation of method_missing
asks the target object whether it respond to a certain method, and that target object probably does inherit from Object
, like most Ruby objects. It is a complex interplay, but at the end of the day, the interface we end up with (i.e. the RecommendedMovies
class) is very simple and intuitive. Look out for those appropriate areas in your code where this pattern might come in handy— you probably have quite a few, and this refactor is usually very pleasing!
If you have any remarks or questions about this post, please use the comments! And if you found it interesting or useful, please support it by clicking the 👏 below.