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.

This work, unless otherwise expressly stated, is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States License.
Related posts:
