Skip to content

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 is satisfied, though, there’s nothing wrong with a little good old-fashioned inheritance.

Applicability

Consider using inheritance when:

  • You control object creation.

2 Comments

  1. Pat Maddox wrote:

    I think this is a perfect example of where to prefer composition over inheritance. I would probably do:

    class BufferedReader
    def initialize(connection)
    @connection = connection
    end

    def receive
    buffer = “”
    begin
    message = @connection.receive
    buffer << message.data
    end until(message.data.include?(“ENDENDEND”))
    Message.new(buffer)
    end
    end

    This allows you to combine different readers to achieve the behavior you want. Not only that, but now the buffered reader is generally useful instead of being coupled to the Connection class.

    Saturday, March 29, 2008 at 1:05 pm | Permalink
  2. avdi wrote:

    Pat: I understand your point, but this was the inheritance post :-) Composition will come later.

    Saturday, March 29, 2008 at 1:37 pm | Permalink

3 Trackbacks/Pingbacks

  1. […] fields are marked * Name * Email * Website Comment add_openid_to_comment_form() ‹ Sustainable Development in Ruby, Part 1: Good Old-Fashioned Inheritance © 2008 ¶ Thanks, WordPress. ¶ veryplaintxt theme by Scott Allan Wallick. […]

  2. […] Development in Ruby (part 1 and part […]

  3. […] This is certainly a leap and a bound beyond global duck punching, where you need to know how every use of that module is going to behave, and then validate that it works out in all those cases. The reality in my experience is that people who pull off global duck punching tend to just kind of pray and hope their unit tests catch any bug they just introduced. This makes me unoptomistic about the maintainability of that code — and even the Ruby community is starting to agree with me (cite, cite). […]

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*