Sustainable Development in Ruby, Part 1: Good Old-Fashioned Inheritance

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.

h3. Applicability

Consider using inheritance when:
* *You* control object creation.