Building Reusable Object-Oriented Systems: Modules

Joël Quenneville

If you can’t fix it with duct tape, you haven’t used enough

~ American folk saying

In a previous post, we looked at building an API adapter for fetching directors from a service called movie-facts.com. It comes in two flavors, read from HTTP and read from cache. The shared logic between the two of these is extracted to a base class.

Paginated requests

After a few weeks in production, you get a bug report. The directors page only displays 10 directors even though the movie-facts.com API contains many more. You try to duplicate the problem locally and quickly realize that the movie-facts.com API is paginated, 10 per page. Unfortunately, they seem to hard code that 10 results per page. You’ll need to make multiple requests and combine the results together.

In the previous example, following the principle

Separate things that change from those that stay the same

led us to using inheritance in a way that allowed us to easily extend and change the system in the future.

However, we now have two independent ways for logic to vary. There are (2 ways to fetch data) X (2 ways of de-paginating data) = 4 combinations. These are:

  1. DepaginatedHttpClient < ClientBase
  2. PaginatedHttpClient < ClientBase
  3. DepaginatedCacheClient < ClientBase
  4. PaginatedCacheClient < ClientBase

Our inheritance hierarchy now looks like:

Combinatorial explosion

The logic is duplicated and reused in many places.

We could try to get clever and add another layer of inheritance to remove some of the duplication:

  1. HttpClient < ClientBase
  2. DepaginatedHttpClient < HttpClient
  3. CacheClient < ClientBase
  4. DepaginatedCacheClient < CacheClient

Sort of fix combinatorial-explosion

There is still duplication of the de-pagination logic and this solution won’t scale once we add more fetching classes or a third independent thing that can change. We are going to need something more powerful than simple inheritance.

Modules

Ruby implements multiple inheritance via modules (often referred to as “mixins”). We can define a module with our de-pagination logic:

module Depaginatable
  def fetch_depaginated_data
    # make multiple calls to `fetch_data`
    # combine results together
  end
end

Now the child classes look like:

module MovieFacts
  class HttpClient < ClientBase
    include Depaginatable

    private

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

and

module MovieFacts
  class CacheClient < ClientBase
    include Depaginatable

    private

    def fetch_data
      # read data from cache
    end
  end
end

Multiple inheritance

We’ve now eliminated all duplication and can now extend our system without too much effort. Success!

Rails uses this approach heavily, taking advantage of modules and concerns (a specialized type of module provided by Rails) to add multiple inheritance throughout the its source.

Limitations of multiple inheritance

Multiple inheritance solved our combinatorial explosion problem. Mostly. Note that the method in our module is fetch_depaginated_data and now fetch_data. We needed to be able to make both normal and de-paginated requests.

Code that uses one of our clients needs to know about the difference and make a decision about which one it wants to use.

We can’t fully leverage polymorphism and create an object that responds to fetch_data, and would return the correct (raw/de-paginated) from (http/cache).

Like simple inheritance, multiple inheritance suffers from encapsulation issues. Although we’ve used some clever tricks to prevent duplicating code, we’ve still duplicated logic across our clients.

We are using a single object to do all the things. An HttpClient instance knows how to fetch data, de-paginate it, and convert it into Director instances. Because a sub-class is all of its ancestors combined, an HttpClient instance looks more like this:

`HttpClient` is all of its ancestors combined

There are no clearly defined responsibilities. Each ancestor has access to the private methods and instance state of the combined object. Since there are no boundaries, it’s easy to refactor the implementation of a function in an ancestor in a way that doesn’t change its behavior and yet still break one of the descendent classes.

In the next article, we address these concerns by looking at composition as an alternate implementation approach.

Further Reading

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

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