A Delegate Matter

From time to time I hear rallying cries to use the Ruby standard library instead of building things from scratch. One piece I’ve been experimenting with is delegation. What I’ve learned is that delegation isn’t all rainbows and unicorns. It still complects your program, and you still have have a deep understanding of what is going on in order to understand the problems that arise.

Meet the Delegation

delegate.rb is a piece of pure Ruby included in the standard library. The source is readable, if a little obtuse if you aren’t comfortable with Ruby metaprogramming. It includes some comment documentation, which provides a few usage examples.

Delegation is forwarding of responsibilities to something else. In object-oriented programming, it means sending some or all messages to another object. In Ruby’s case, it generally means that you want to delegate all messages except for the one’s explicitly defined on the object.

Ruby’s delegate.rb defines three classes.

  • Delegator is intended as an abstract base class for the others. You could use it directly, but you usually won’t.
  • SimpleDelegator seems to be intended for use when you want to hand out a reference to an object, and then change the object that is actually receiving messages without having to tell everybody you gave the reference too, a kind of proxy. I’ve used it as a superclass without much trouble, to override a few methods and forward the rest.
  • DelegateClass, the comments claim, should be the most common use case. It expects you to name the class of the object you want to forward to, at which point it defines forwarding methods for exactly that set of methods. This requires having a class, possibly abstract, that represents the set of messages you might want to send. I haven’t used it because I tend towards the message-sending view of OOP, where you don’t need to know the type of the object beforehand.

I was using SimpleDelgator as a superclass to set up a kind of psuedo-prototypal relation. I was was fearful of doing a more direct prototypal style. I really out to benchmark it some time.

In any case, I set up series of tests and my delegation scheme worked well.

And Then, I Added Rails

Some time ago, I added the necessary bits to serve XML and JSON from the Rails app, which at the moment is mostly serving as an API backend. That also got set up and worked fine.

Later, I took some of the objects being returned through the API, and wrapped some SimpleDelegator Subclasses around them, to provide information enhanced by another API. This seemed to work well, and the HTML results reflected the updated information.

You probably noticed I said “HTML results”. When I went back to grab a JSON snapshot for another test, I found a problem. (Actually I found a couple problems, but I’m only going to discuss one here.)

The JSON results were showing data for the unwrapped object. I re-ran my tests and everything was fine. I looked at the HTML results, and they were splendid. For some reason, when Rails tried to convert my object into JSON, it grabbed the wrong data.

It may be worth noting that the object included a custom to_hash method, and earlier experimentation had proven that this method was used by the JSON serialization routines to convert objects. (Unless they are ActiveRecord objects, in which case you need a serializable_hash. Okay, so I’m discussing two bugs.)

One possible explanation is that when the delegated object gets to_hash, it uses it’s own properties instead of going through the delegator. The problem is I foresaw this issue, and I had the tests to prove that it worked. Outside Rails.

Lets get perfectly clear on what is happening. For the sake of argument, lets pretend we are getting data from service A, say a movie database. We then want to call a second service B, and use data from this second service to update how we represent the user. Let’s say B is a list of playtimes at nearby theaters. So I want to return one result for each play time, with each item providing a time attribute, and overriding the title to include the time.

When we ask a result for it’s rating, the call goes

Playtime:22 => Movie:11#rating

Where I’m using ’11′ to stand in for object identity. When we ask for the title, it returns the modified one:

Playtime:22#title

The actual ancestor chains are a bit more more complicated, but this is what matters for our purposes. Lets just get one thing straight: say we haven’t redefined a basic method, like inspect. It will get handled because it’s defined on Object.

Playtime:22 => Movie:11 -> Object:11#inspect

If you are paying careful attention, I’ve already let the cat out of the bag, so to speak. But there’s still a plot twist to come.

To catch Rails about it’s dirty business, I sent in a spy.

class Spy
  def to_hash
    {}
  end

  def method_missing(method, *args)
    log.debug {"CALL: #{method}(#{args.inspect}) {#{block_given?}}"}
    log.debug {caller(6).take(2)}
    return nil
  end

  def respond_to?(method)
    log.debug {"PROBE: #{method}"}
    log.debug {caller(6).take(2)}
    super
  end
end

What this tells us is that Rails is probing, in order (respond_to?) as_json and then to_hash

So, it checks for as_json, doesn’t find it, and then checks for to_hash, right?

The backtrace rewards investigation:

activesupport-3.1.1/lib/active_support/json/encoding.rb:149:in `as_json'

Wait, we didn’t have an as_json to call right? Why did it go on to check for to_hash?

149:    if respond_to?(:to_hash)

But it says it’s in as_json?

148:  def as_json(options = nil) #:nodoc:

Wait for it….

147: class Object

Oh. I guess we did have one. Three cheers for “Freedom Patching”. (I think I heard three… somethings.)

Okay, so we’ve got an as_json method. It just sniffs out to_hash and calls it. Big deal right?

Remember how I helpfully included object identity in the message diagrams earlier?

Playtime:22 => Movie:11 -> Object:11#as_json

Perfectly logical, right. But we have to be very careful about understanding of message sending. When an object sends a message (to_hash) to itself, it doesn’t know anything about upstream delegators.

Movie:11 -> Object:11#to_hash

And so our JSON has the Movie version of the properties, without any helpful playtimes.

Update: I seem to have rediscovered self schizophrenia.

Being A Little Forwardable

The comments in delegate.rb include a helpful pointer to Forwardable, another standard library module. Forwardable requires you to explicitly list messages to be forwarded.

extend Forwardable
def_delegators :@movie, :rating, :type

Since the object forwarded is explicit, you can also send messages to different objects. As an added bonus, Forwardable is a module instead of a class, so it doesn’t take up the one and only inheritance slot.

What is significant for the present purpose is that Forwardable won’t forward unknown messages like as_json. Since the message doesn’t get passed down, the object identity doesn’t change, and we don’t get such surprising behavior.

Playtime:22#rating => Movie:11#rating

Playtime:22 -> Object:22#as_json
Playtime:22#to_hash

There is another module called SingleForwardable that I don’t completely understand. I think it sends the messages to a module instead of an instance variable.

Posted Friday, January 6th, 2012 under War Story.

Comments are closed.