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