Skip to content

Sustainable Development in Ruby, Part 2: Method Injection

Sometimes you have a need for an object method which the class author did not foresee. For instance, in our previous installment, we used the following code to accumulate packets until an ending packet was found:

  class BufferedConnection < FMTP::Connection
    def receive
      buffer = ""
      begin
        message = super
        buffer << message.data
      end until(message.data.include?("ENDENDEND"))
      Message.new(buffer)
    end
  end

We test whether the packet denotes the end of a message by searching for the token "ENDENDEND". This is a little messy. It would be cleaner if we could call a predicate method on message to determine whether it indicates the end of a multi-packet message.

It’s easy enough to re-open the Messsage class and add such a method:

  class FMTP::Message
    def end?
      data.include?("ENDENDEND")
    end
  end

Let’s take a step back, however. While adding a previously undefined method is one of the more benign forms of runtime class modification, it is not without its risks. And in this case, we only need the #end? method in one place in our own code, which hardly justifies modifying the Message class globally. Instead, we could localize the extension by injecting the method just in time:

  class BufferedConnection < FMTP::Connection
    def receive
      buffer = ""
      begin
        message = extend_message(super)
        buffer << message.data
      end until(message.end?)
      Message.new(buffer)
    end
 
    private
 
    def extend_message(message)
      def message.end?; data.include?("ENDENDEND"); end
      message
    end
  end

In the new method #extend_message, we are using Ruby’s dynamic nature to add a new method to the message object at runtime. Now our extension is scoped only to the code that needs it.

There is one more small benefit to using this technique over re-opening the class: our extension is not bound to a particular class in the FMTP library. We don’t have to worry about which class to patch, or even if Connection#receive might return more than one type of message. So long as the object returned by #receive contains a #data method, our extension will continue to work.

Applicability

Consider using dynamic method injection when:

  • Vendor code controls instantiation of the target
  • Your code is the primary client of the target
  • The extension is only needed in a small subset of the code.

Stay tuned for our next episode, in which we’ll talk about delegation.

2 Comments

  1. Shalev wrote:

    A few points:

    1. This strikes me as fairly inefficient. Assuming this receive loop works for a while, you’ve just created a large number of objects and then dynamically defined a method on each one.

    2. You state that “adding an undefined method…is not without its risks,” but you fail to state what those risks are.

    3. “we only need the #end? method in one place” This may be true now, but surely the method may be useful further down the line. What’s more, if this really is true, I don’t see your logic for creating a special #end? method anyway. message.data.include?(“ENDENDEND”) isn’t a large statement and it has the added advantages of being clearer and more specific (without requiring someone reading your code to jump down to the #end? method to see what the end condition is.) Normally you would move code into a method in order to reuse it. If you’re not going to do that, then there really isn’t much of a point to creating a separate method.

    Monday, April 14, 2008 at 10:27 pm | Permalink
  2. avdi wrote:

    Shalev:

    1. To quote Knuth, premature optimization is the root of all evil ;-) Considering that our mythical Flying Monkey Transport Protocol probably has a packet latency measured in hours, I suspect that this is the least of their performance problems.

    2. See the introduction to this series. If two separate pieces of code independently try to add the same method, it’s no longer a method addition and becomes an inadvertent method redefinition.

    3a. I apply YAGNI pretty religiously. I used to code based on the “we’ll need it someday” principle a long time ago, and it never got me anywhere good.

    3b. Reuse is not the only reason to extract code into a method. Other reasons include readability and enforcing the Single Responsibility Principle. The code that calls #end? is not concerned with how we know that the message is at the end, but with what to do if it is. Having the implementation details of how we know at that point is a distraction from the core purpose of the code.

    But this kind of misses the point anyway, because this is demo code which I have deliberately kept simple in order to focus on the techniques being presented. In the real world #end? might very well be a more complicated (and distracting) algorithm.

    Thursday, April 17, 2008 at 8:16 am | Permalink

2 Trackbacks/Pingbacks

  1. […] Sustainable Development in Ruby (part 1 and part 2) […]

  2. […] our last episode we were augmenting FMTP::Message classes to deal with messages split across multiple packets. As is […]

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*