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”:http://avdi.org/devblog/2008/03/27/sustainable-development-in-ruby-part-1-good-old-fashioned-inheritance/, 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.

h3. 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.

10 comments

  1. 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.

  2. 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.

    1. It also has the advantage of still allowing the Message object and the class needing to use the end? functionality to still remain orthogonal to each other.

      I pity the man that upgrades the FMTP library only to find out that end? has been implemented and used there, and runs that code.

  3. 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.

  4. 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.

  5. An alternative implementation of the same idea presented in this article is to create a module that has an :end? method and then extend the method with that module. Extending objects with a module also addresses some of the performance concerns associated with creating a metaclass for each message.

    class BufferedConnection < FMTP::Connection
    module Extension
    def end?
    data.inclue?('ENDENDEND')
    end
    end

    def extend_message(message)
    message.extend(Extension)
    end
    end

    With this implementation, perhaps it would be simpler to do extend the message inline rather than calling a method named extend_message with simply calls extend on the method:
    before: message = extend_message(super)
    after: message = super.extend(Extension) # and removal of the extend_message method

  6. An alternative implementation of the same idea presented in this article is to create a module that has an :end? method and then extend the method with that module. Extending objects with a module also addresses some of the performance concerns associated with creating a metaclass for each message.

    class BufferedConnection < FMTP::Connection
    module Extension
    def end?
    data.inclue?('ENDENDEND')
    end
    end

    def extend_message(message)
    message.extend(Extension)
    end
    end

    With this implementation, perhaps it would be simpler to do extend the message inline rather than calling a method named extend_message with simply calls extend on the method:
    before: message = extend_message(super)
    after: message = super.extend(Extension) # and removal of the extend_message method

Comments are closed.