Sustainable Development in Ruby, Part 3: Delegation

In our “last episode”: we were augmenting @FMTP::Message@ classes to deal with messages split across multiple packets. As is often the case, fixing one problem revlealed another. What with the unstable weather patterns in Oz – you never know when a spacetime-ripping tornado will appear out of nowhere – our flying monkeys sometimes get blown off course, and arrive out of order. This results in jumbled messages and angry Wicked Witches.

We’ve submitted a revised FMTP RFC to the Flying Monkey Transport Protocol Working Group, but it’s anyone’s guess how long that will take to become an official recommendation. Until then, we’ve taken to embedding another bit of metadata in the message text itself. Messages now look like this:

In order to work conveniently with these enhanced messages, we need some new accessors on the @FMTP::Message@ class:

  • A revised @#end?@ method which uses the new header instead of the now-deprecated “ENDENDEND” token.
  • A new @#seq_num@ accessor to tell us which message in a multi-message sequence this is.
  • A new @#total_num@ accessor to tell us how many total messages are expected as part of the sequence.
  • A modified @#data@ accessor which will return just the message data, minus the message ordering header.

Sounds like it’s time to inject some more methods. But not so fast. I prefer to use the inject method pattern when I only need to add one trivial method. When I need to add or modify more than one method, a few other techniques are better suited. One of the most powerful techniques is delegation.

“Delegation”: has a long history of use in object-oriented languages. In some languages using it can be quite labor-intensive to implement. In Ruby it is so trivially easy that it’s a little surprising it isn’t used more often.

Here’s an example of a delegate class that implements the requirements above:

We use this class by replacing the line that previously read:

With this code:

There are a few things worth noting about this approach:

  • Using a delegate gives us a safe namespace sandbox to play in. This code defines four methods, a constant, and three instance variables. In particular, note that we define our own @data instance variable. If we were monkey patching, or even subclassing, @FMTP:Message@, we would run the risk of inadvertently overriding or overwriting one of the @FMTP::Message@ constants, methods, or instance variables. With delegation, however, we can define pretty much anything we want without having to be concerned with collisions.
  • Note that @#data@ uses @super@ to delegate to @FMTP::Message@, just as if we were writing a subclass. This is a convenience of using the standard @delegate@ library.
  • [NEW] While it’s not really demonstrated in the code above, I should point out that any @FMTP::Message@ methods not explicitly overridden on @OrderedMessage@ will be delegated directly to the wrapped @FMTP::Message@ object.
  • An added benefit of using the delegate pattern is that it is very easy to test our additions to @FMTP::Message@ in isolation. Here is the actual RSpec spec I used to develop the code above:

Using delegation to extend @FMTP::Message@, we don’t need to instantiate an actual @FMTP::Message@ object in order to test our modifications. All we have to do is stub the methods of @FMTP::Message@ that we actually use in the delegate. This can be a real win when we are working with a third-party library which has extensive and/or poorly-documented dependencies.

h3. Applicability

Consider using delegation when:
* Vendor code controls instantiation of the target.
* Your code is the primary client of the target.
* You need to make more than one addition or modification to the target’s interface.

h3. Caveats

Be aware that delegates produces with the @delegate@ library are not perfect stand-ins for their target objects. In particular, by default the delegate object will have a different @id@ and @object_id@. This is easy to correct, but you should be aware of it, especially when working with @ActiveRecord@, which uses @id@ to associate objects with rows in the DB.