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 @[email protected] to determine whether it indicates the end of a multi-packet message.
It’s easy enough to re-open the @[email protected] 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 @#[email protected] method in one place in our own code, which hardly justifies modifying the @[email protected] 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 @#[email protected], 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 @[email protected] library. We don’t have to worry about which class to patch, or even if @Connection#[email protected] might return more than one type of @[email protected] So long as the object returned by @#[email protected] contains a @#[email protected] 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.