Refactoring From Inheritance To Composition To Data

??? words · ??? min read

“Prefer composition over inheritance” is a popular saying amongst programmers. In this article, I want to demonstrate what that looks like.

We’re going to refactor some Rails classes from inheritance to composition. Then we’ll refactor again from composition to something even better: data.

Inheritance (The Original Code)

In an attempt to show “real” code, I’ve taken this example from the Rails codebase. These classes originally came from action_view/helpers/tags. However, I have simplified the code by removing some of the unrelated parts.

This implementation starts with DatetimeField:

class DatetimeField
  def render(date)
    value = format_date(date)
    type = self.class.field_type
    "<input type='#{type}' value='#{value}' />"
  end

  def format_date(date)
    raise NotImplementedError
  end

  def self.field_type
    'datetime'
  end
end

This class is used to render HTML input tags that contain dates/times, for example:

DatetimeField.new.render(Time.now)
#=> "<input type='datetime' value='2017-06-26T20:28:20' />"

The above code will not work, however, because DatetimeField is an abstract base class. Notice the NotImplementedError raised in format_date. This class is designed to be inherited from, and the subclasses must override format_date.

There are four subclasses that inherit from DatetimeField:

class DatetimeLocalField < DatetimeField
  def self.field_type
    "datetime-local"
  end

  def format_date(value)
    value.try(:strftime, "%Y-%m-%dT%T")
  end
end

class DateField < DatetimeField
  def format_date(value)
    value.try(:strftime, "%Y-%m-%d")
  end
end

class MonthField < DatetimeField
  def format_date(value)
    value.try(:strftime, "%Y-%m")
  end
end

class WeekField < DatetimeField
  def format_date(value)
    value.try(:strftime, "%Y-W%V")
  end
end

So the output of render depends on which subclass is being used. This is essentially the template method design pattern.

Refactoring To Composition

This refactoring involves separating the base class from all the subclasses. Each subclass will be turned into a collaborator – a separate object that the base class uses. Where there used to be a single object, there will now be two different objects.

The first question to ask is: how does the base class use the subclasses?

  1. The base class calls format_date to turn a date into a string.
  2. The base class allows subclasses to override the default field_type using a class method.

This will be the interface between the two objects.

Secondly, we need a name for these collaborator objects. It appears to me that the subclasses are date formatters, not date fields, so let’s call these collaborators “formatters”.

If we were using a statically-typed language, this is the point where we would define the interface. For example, this is what the interface would look like in Swift:

protocol DatetimeFieldFormatter {
  func format(date: DateTime) -> String
  var fieldType: String { get }
}

But this is Ruby, and we don’t have interfaces, we have duck types instead. A duck type is a kind of implicit interface. We still have to conform to the interface by implementing all the expected methods, but the expected methods haven’t been declared with code.

Knowing the interface, we can refactor the base class to use a collaborator called formatter:

class DatetimeField
  attr_reader :formatter

  def initialize(formatter)
    @formatter = formatter
  end

  def render(date)
    value = formatter.format_date(date)
    type = formatter.field_type || 'datetime'
    "<input type='#{type}' value='#{value}' />"
  end
end

The collaborator is provided as an argument to initialize, and stored in an instance variable. Later on, it is used to format dates, and possibly override the tag type. This is an example of dependency injection.

Now lets refactor all the subclasses into classes that implement the formatter interface:

class DatetimeLocalFormatter
  def format_date(value)
    value.try(:strftime, "%Y-%m-%dT%T")
  end

  def field_type
    "datetime-local"
  end
end

class DateFormatter
  def format_date(value)
    value.try(:strftime, "%Y-%m-%d")
  end

  def field_type
    nil
  end
end

class MonthFormatter
  def format_date(value)
    value.try(:strftime, "%Y-%m")
  end

  def field_type
    nil
  end
end

class WeekFormatter
  def format_date(value)
    value.try(:strftime, "%Y-W%V")
  end

  def field_type
    nil
  end
end

Notice how all the classes have been renamed to Formatter, and they don’t inherit from anything.

Now rendering the output is done like this:

DatetimeField.new(WeekFormatter.new).render(Time.now)
#=> "<input type='datetime' value='2017-06' />"

Bonus Refactor: Singletons

Let’s take a short detour into functional programming town.

The formatter classes don’t have any instance variables. To put it another way, they contain only pure functions and no state. All instances of the same class will be identical, so we only ever need to have one. This is why they don’t need to be classes.

In this situation, I prefer to use singleton modules:

module WeekFormatter
  extend self

  def format_date(value)
    value.try(:strftime, "%Y-W%V")
  end

  def field_type
    nil
  end
end

Now WeekFormatter is an object that contains all the methods, and you don’t ever need to use new. It is essentially a namespace of functions.

End of detour.

Refactoring To Data

Savvy readers may have noticed that the refactored formatter objects don’t contain much code. There is duplication too, because every formatter contains value.try(:strftime, ...). In fact, only two strings are differ between each formatter class: the date format string and the field_type string.

When a class contains data, but doesn’t contain any behaviour, it doesn’t need to be a class. It could just be a Hash instead.

If we turn all the formatter classes into plain old Hashs, and pull all the behaviour up into the DatetimeField class, it would look like this:

class DatetimeField
  FORMATS = {
    local: { strftime_format: "%Y-%m-%dT%T", field_type: "datetime-local" },
    date:  { strftime_format: "%Y-%m-%d" },
    month: { strftime_format: "%Y-%m" },
    week:  { strftime_format: "%Y-W%V" },
  }

  attr_reader :format

  def initialize(format_key)
    @format = FORMATS.fetch(format_key)
  end

  def render(date)
    value = date.try(:strftime, format.fetch(:strftime_format))
    type = format.fetch(:field_type, 'datetime')
    "<input type='#{type}' value='#{value}' />"
  end
end

Rendering the output is done like this:

DatetimeField.new(:week).render(Time.now)
#=> "<input type='datetime' value='2017-06' />"

This is the best implementation, in my opinion. Five classes were reduced to a single class. It requires less code. Everything is closer together, instead of being spread across multiple files.

Not to be confused with domain-driven design (DDD).

This is an example of what I call “data-driven design” or just “thinking in data”. All the specific details are described in data: numbers, strings, arrays, hashes, structs, etc. All the behaviour (i.e. the implementation) is generic, and controlled by the data.

I really like this approach. You can use it any situation where the details can be represented as pure data, with no behaviour. But when different cases require significantly different behaviour, collaborator objects are the better choice.

Got questions? Comments? Milk?

Shoot an email to [email protected] or hit me up on Twitter (@tom_dalling).

← Previously: Result Objects - Errors Without Exceptions

Next up: Super Secret Methods →

Join The Pigeonhole

Don't miss the next post! Subscribe to Ruby Pigeon mailing list and get the next post sent straight to your inbox.