Nothing is Something

For RailsConf 2015 Sandi Metz gave a fantastic talk with two parts: the first discussed the null object pattern and the second talked about composition over inheritance. This post is some of my thoughts on the second half.

However, before I get started, please go watch her talk: Nothing is Something

Composition Over Inheritance

That talk is one of the best and most clearly communicated examples of the benefits of composition I’ve seen to date. However, I’m writing this because I feel like things could have gone just a step further.

At the end of the talk we’re left with code that looks like this:

class House
  attr_reader :data, :formatter
  DATA = [
    "the horse and the hound and the horn that belonged to",
    "the malt that lay in",
    "the house that Jack built"
  ]
  def initialize(orderer: DefaultOrder.new, formatter: DefaultFormatter.new)
    @formatter = formatter
    @data = orderer.order(DATA)
  end

  def recite
    (1..data.length).map {|i| line(i)}.join("\n")
  end

  def line(number)
    "This is #{phrase(number)}.\n"
  end

  def phrase(number)
    parts(number).join(" ")
  end

  def parts(number)
    formatter.format(data.last(number))
  end
end

class DefaultFormatter
  def format(parts)
    parts
  end
end

class EchoFormatter
  def format(parts)
    parts.zip(parts).flatten
  end
end

class RandomOrder
  def order(data)
    data.shuffle
  end
end

class DefaultOrder
  def order(data)
    data
  end
end

Define Responsibilities

This is great code, but in a step towards refactoring even further it’s important to define responsibilities.

House should have one responsibility: recite. Take a dataset, use something to order and format it, and then output it. However, in order to fulfill that responsibility it must also know how to join an array of terms such that’s recitable and extract a certain piece of those terms for recitation.

Formatters take a phrase represented as an array of terms and formats them to be joined by a space.

Orderers take a full dataset and orders them.

Analyze Dependencies

Formatters have one dependency: an array of terms.

Orderers have one dependency: an array of terms to order.

House has two dependencies: a formatter and an orderer.

Reduce Responsibilities

The Single Responsibility Principle says that each object should only have one reason to change, and House has three right now. Let’s see if we can get it down.

First up is to extract the format piece.

class HouseData
  attr_reader :data, :formatter
  def initialize(data:, formatter: DefaultFormatter.new)
    @data = data
    @formatter = formatter
  end

  def length
    data.length
  end

  def phrase(number)
    parts(number).join(" ")
  end

  private

  def parts(number)
    formatter.format(data.last(number))
  end
end
class House
  attr_reader :data, :formatter
  DATA = [
    "the horse and the hound and the horn that belonged to",
    "the malt that lay in",
    "the house that Jack built"
  ]
  def initialize(orderer: DefaultOrder.new, formatter: DefaultFormatter.new)
    @formatter = formatter
    @data = HouseData.new(data: orderer.order(DATA), formatter: formatter)
  end

  def recite
    (1..data.length).map {|i| line(i)}.join("\n")
  end

  def line(number)
    "This is #{data.phrase(number)}.\n"
  end

end

Now House just knows how to take something that responds to #phrase and #length and recite them. HouseData knows what it takes to turn an array of terms into phrases.

Analyze Dependency Usage

If you look at the constructor for House you’ll notice that orderer is only used to initialize data and formatter is only used to be passed off into HouseData. A good rule is that if dependencies are only used in constructors, extract them and pass the good value in as the parameter instead. This makes your class just that much more flexible.

class HouseData
  attr_reader :data, :formatter
  def initialize(data:, formatter: DefaultFormatter.new)
    @data = data
    @formatter = formatter
  end

  def length
    data.length
  end

  def phrase(number)
    parts(number).join(" ")
  end

  private

  def parts(number)
    formatter.format(data.last(number))
  end
end
class House
  attr_reader :data, :formatter
  DATA = [
    "the horse and the hound and the horn that belonged to",
    "the malt that lay in",
    "the house that Jack built"
  ]
  def initialize(data: data)
    @data = data
  end

  def recite
    (1..data.length).map {|i| line(i)}.join("\n")
  end

  def line(number)
    "This is #{data.phrase(number)}.\n"
  end
end

# Initialization
house_data = HouseData.new(data: House::DATA)
House.new(house_data).recite

You’ll notice that since you’re passing in the data, there’s no need for a “default orderer” - just pass in the data.

Finale

Now you’re left with this:

class HouseData
  attr_reader :data, :formatter
  def initialize(data:, formatter: DefaultFormatter.new)
    @data = data
    @formatter = formatter
  end

  def length
    data.length
  end

  def phrase(number)
    parts(number).join(" ")
  end

  private

  def parts(number)
    formatter.format(data.last(number))
  end
end

class House
  attr_reader :data
  DATA = [
    "the horse and the hound and the horn that belonged to",
    "the malt that lay in",
    "the house that Jack built"
  ]
  def initialize(data: data)
    @data = data
  end

  def recite
    (1..data.length).map {|i| line(i)}.join("\n")
  end

  def line(number)
    "This is #{data.phrase(number)}.\n"
  end
end

class DefaultFormatter
  def format(parts)
    parts
  end
end

class EchoFormatter
  def format(parts)
    parts.zip(parts).flatten
  end
end

class RandomOrder
  def order(data)
    data.shuffle
  end
end

# Initialization
house_data = HouseData.new(data: House::DATA)

random_house_data = HouseData.new(data: RandomOrder.new.order(house_data.data))

echo_house_data = HouseData.new(data: house_data.data, formatter: EchoFormatter.new)

random_echo_house_data = HouseData.new(data: random_house_data.data, formatter: echo_house_data.formatter)

# Recitation

House.new(data: house_data).recite

Everything has one responsibility and is completely flexible. The only other thing I might do is simplify the interface for Formatter and Order. Something like

class EchoFormatter
  def self.call(parts)
    parts.zip(parts).flatten
  end
end

class DefaultFormatter
  def self.call(data)
    data
  end
end

class RandomOrder
  def self.call(data)
    data.shuffle
  end
end

would make it easier to remember what to do to use the dependencies.

Compliments

I just want to leave a final note of thanks to Sandi Metz for an excellent talk. It provided a much better example of composition over inheritance for my team than I’ve seen before. Highly recommended.