Building Reusable Object-Oriented Systems: Inheritance

Joël Quenneville

When building software, we often come across special cases, specializations, and shared logic. In object-oriented languages, inheritance is commonly used to deal with these.

Building an API client

Let’s say you are writing a client to interact with a third-party API that lists movies. It might look like:

module MovieFacts
  class Client
    def initialize(client_id, client_secret)
      @client_id = client_id
      @client_secret = client_secret
    end

    def directors
      fetch_data("/directors").map { |director| Director.new(director) }
    end

    def director(name)
      Director.new(fetch_data("/directors/#{name}"))
    end

    private

    def fetch_data
      # fetch directors from API
    end
  end
end

The client returns a lightweight Director object based on the JSON response.

module MovieFacts
  class Director
    def initialize(json)
      @raw_data = JSON.parse(json)
    end

    def name
      @raw_data.fetch("name")
    end

    def id
      @raw_data.fetch("id")
    end
  end
end

Inheritance

A new set of requirements come in. movie-facts.com is rate limiting your service so you need to watch how many requests you make in a day. The good news is that you know that movie-facts.com only updates its systems once a day so it should be trivial to cache the data in memory and only fetch from the API once a day. Not only does this fix your rate-limiting issues but it also speeds up performance of your own system.

There are now two ways of doing the same task (and it’s not too hard to imagine others coming along down the road). Creating a separate class would result in a lot of duplicated code.

Duplicated code

The intro to “Design Patterns in Ruby” describes an additional principle:

Separate things that change from those that stay the same

The logic for returning the list of director objects remains the same, but the way we fetch the data changes based on context. We need a way to separate the fetching logic (which changes), from the director logic (which doesn’t).

The most commonly used solution for this problem is inheritance. Inheritance is a way of creating a specialized form of an object. Because of this, child classes have access to all the methods and private state of their parent. They are their parent, with a few modifications.

Extracting a base class

You start by extracting the shared logic into a ClientBase class

module MovieFacts
  class ClientBase
    NotImplementedError = Class.new(StandardError)

    def initialize(client_id, client_secret)
      @client_id = client_id
      @client_secret = client_secret
    end

    def directors
      fetch_data("/directors").map { |director| Director.new(director) }
    end

    def director(name)
      Director.new(fetch_data("/directors/#{name}"))
    end

    private

    def fetch_data
      raise NotImplementedError
    end
  end
end

You then derive a class for handling fetches via HTTP:

module MovieFacts
  class HttpClient < ClientBase

    private

    def fetch_data
      # make HTTP request
      # cache response
    end
  end
end

and one for fetches from cache.

module MovieFacts
  class CacheClient < ClientBase

    private

    def fetch_data
      # read data from cache
    end
  end
end

Now if you ever want to add a third way to fetch the data in the future, all you need to do is add a new sub-class. Great use of the Open/Closed principle!

Limitations of inheritance

Simple inheritance solved our duplicated logic problem. It also made it easy for us to extend the system to fetch data from other sources if necessary. However, it does have a few critical weaknesses when it comes to building reusable OO components:

  1. it is vulnerable to combinatorial explosion when there are multiple independent parts of the code that vary.
  2. there is no encapsulation between parents and descendants.

We dig into how to solve this problems in the next posts on modules and composition.

Further Reading

This article is part 1 of 4 in a series on building reusable object-oriented software.

  1. Simple Inheritance (this article)
  2. Mixins/Multiple Inheritance
  3. Composition
  4. Composition vs Inheritance