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 Movies, 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.