The first technique we’ll look at in this series is something so basic it may not even seem worth spelling out. But sometimes old-school techniques are overlooked in the excitement of a young language.
Let’s use as our example a hypothetical communications protocol, Flying Monkey Transport Protocol (FMTP). Flying Monkey Transport Protocol is a packet-based peer-to-peer networking protocol in which messages are transported from one peer to another by means of flying monkeys carrying satchels full of data.
As developers in the inter-kingdom IT department, it’s our job to make sure that communications between e.g. the Wicked Witch of the East and the Lollipop Guild flow unimpeded. Where wicked witches are concerned it’s important that no one get mixed messages.
The interface for the Ruby FMTP implementation looks something like this:
module FMTP class Connection def initialize(address) # ... end def send(message) # ... end def receive # ... end end class Message # ... attr_reader :data end end
Once a connection is initialized, we can receive messages from the opposite peer by calling receive, which returns a @Message@ object:
connection = FMTP::Connection.new("witch.east") message = connection.receive
Lately the Wicked Witch of the East has gotten rather chatty in her old age, and her messages have been exceeding maximum monkey capacity. As a result, we’ve been forced to start dividing her messages up across multiple monkeys. Unfortunately, the writers of the FMTP library did not plan for this possibility, so recipients of the Witch’s communiques have been getting truncated messages. We’ve been tasked with making the necessary changes in order to support multi-monkey messages.
As good Ruby programmers, we like to exploit the language’s dyanamic features to the max. And at first, this might seem like the perfect opportunity to use Ruby’s capacity for runtime class modification. We’ll just re-open the class and patch it to do what we need:
module FMTP class Connection alias_method :receive_without_buffer, :receive def receive buffer = "" begin message = receive_without_buffer buffer < < message.data end until(message.data.include?("ENDENDEND")) Message.new(buffer) end end end
But there's another way to accomplish the same ends. A simpler, low-tech way: inheritance.
Inheritance has gone somewhat out of fashion in recent years. And not without reason. In the old days inheritance was seen as almost synonymous with object-orientation, and as a result it was frequently abused. Programs would consist of elaborate, many-leveled inheritance heirarchies that resembled an inbred royal family tree. These programs were hard to understand and hard to maintain.
Ruby programmers have, for the most part, learned their lesson well in this regard. I rarely see a Ruby application with more than two layers of inheritance. For the most part this is a good thing. But occasionally the avoidance of inheritance leads to implementing more complex solutions in places where inheritance is a perfectly legitimate technique.
This is one of those cases. Here is how the code would look using inheritance:
class BufferedConnection < Connection def receive buffer = "" begin message = super buffer << message.data end until(message.data.include?("ENDENDEND")) Message.new(buffer) end
And here's how it's used:
connection = BufferedConnection.new("witch.east") message = connection.receive
The difference is small, to be sure. But the inheritance version has a number of advantages. It’s slightly shorter. It’s simpler, because there is no need to alias the original method to a new name; we can just use @super@. The name @BufferedConnection@ makes it obvious that we are using a buffered variant of a @Connection@. There’s no chance of our becoming confused by the disparity between what the original @#receive@ method says, and how it actually behaves. And we know that since we have to explicitly ask for the buffered version, there’s no chance of our inadvertantly breaking code somewhere else in the program by changing the semantics of @Connection@.
It might seem like too obvious a technique to even mention. But it’s easy to forget about the prosaic solutions in a language that gives us so many possibilities. You should still put some thought into whether inheritance isappropriate in any given situation. So long as it is a legitimate IS-A relationship and the “Liskov Substitution Principle”:http://c2.com/cgi/wiki?LiskovSubstitutionPrinciple is satisfied, though, there’s nothing wrong with a little good old-fashioned inheritance.
Consider using inheritance when:
* *You* control object creation.