dom lizarraga

dominiclizarraga@hotmail.com

POOD Session 11: Escalation, Loosen Coupling in Tests & Nothing is Something

27 minutes to read

Session 11: Escalation, Loosen Coupling in Tests & Nothing is Something

Date: October 18, 2025

This blog post consists in four parts:

Key Concepts

Escalation

This block introduces a new requirement: Escalation #2-Mix up Actors and Actions. This triggers a whole bunch of refactorings and writing of new tests, after which the code ends up both simpler and more abstract.

Watch 1: Seeking Abstractions

New requirement is split the data into two arrays, one named ‘actor’ and the other ‘action’.

Example:

# The trailing 'that' in each line below separates two things: 'actor' and 'action'
# actors:
# 'the horse and the hound and the horn' or 'the rooster that crowed in the morn'
#
# actions
# 'that belong to' or'that woke'
#
# Create another variant that randomly mixes actors and actions.
# ------------actor-----------------------action---------
# Thus, 'the priest all shaven and shorn', 'that killed', etc.

As you work, consider these questions:

  1. Should you split the DATA into two arrays, one named ‘actor’ and the other ‘action’? Or is there a more general way to satisfy the actor/action requirement?

  2. This is the first requirement that involves that DATA array. Is it time to isolate the DATA in a separate class?

  3. If the DATA moves to a separate class which is then injected back into House, should the injected data come in the correct order, or should House still be responsible for ordering it?

As we look here at the current House class, if you were to ask yourself, “Does it have more than one responsibility?” or perhaps “If it did two things, what would those two things be?” the answer is, probably, the DATA feels like one kind of thing, and the code, the algorithm, feels like another.

Let’s start by writing some tests for the new classes before we extract DATA from House:

class OriginalOrdererTest < Minitest::Test
  def test_order
    input = %w(a b c d e)
    expected = input
    assert_equal expected, OriginalOrderer.new.order(input)
  end
end

class RandomOrdererTest < Minitest::Test
  def test_order
    Random.srand(1)
    input = %w(a b c d e)
    puts input
    expected = %w(c b e a d)
    assert_equal expected, RandomOrderer.new.order(input)
    Random.srand
  end
end

class RandomLastOrdererTest < Minitest::Test
  def test_order
    Random.srand(1)
    input = %w(a b c d e always_last_item)
    expected = %w(c b e a d always_last_item)
    assert_equal expected, RandomLastOrderer.new.order(input)
    Random.srand
  end
end

This is the code so far once we have extracted the DATA into its own class, making sure it provides the right order since it’s not a House responsability:

class Phrases
    DATA =
    [ ["the horse and the hound and the horn", "that belonged to"],
      ["the farmer sowing his corn", "that kept"],
      ["the rooster that crowed in the morn", "that woke"],
      ["the priest all shaven and shorn", "that married"],
      ["the man all tattered and torn", "that kissed"],
      ["the maiden all forlorn", "that milked"],
      ["the cow with the crumpled horn", "that tossed"],
      ["the dog", "that worried"],
      ["the cat", "that killed"],
      ["the rat", "that ate"],
      ["the malt", "that lay in"],
      ["the house", "that Jack built"]
    ]

  attr_reader :data

  def initialize(orderer: OriginalOrderer.new)
    @data = orderer.order(DATA)
  end

  def phrase(num)
    data.last(num).join(" ")
  end

  def size
    data.size
  end
end

class House
  attr_reader :phrases, :prefix

  def initialize(phrases: Phrases.new, prefixer: MundanePrefixer.new)
    @phrases = phrases
    @prefix = prefixer.prefix
  end

  def recite
    1.upto(phrases.size).collect {|i| line(i)}.join("\n")
  end

  def phrase(num)
    phrases.phrase(num)
  end

  def line(num)
    "#{prefix} #{phrase(num)}.\n"
  end
end


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

class OriginalOrderer
  def order(data)
    data
  end
end

class RandomLastOrderer
  def order(data)
    data[0..-2].shuffle << data.last
  end
end

class PiratePrefixer
  def prefix
    "Thar be"
  end
end

class MundanePrefixer
  def prefix
    "This is"
  end
end

puts
phrases = Phrases.new(orderer: OriginalOrderer.new)
puts House.new(phrases: phrases).line(12)

Now that we have separated these bits of code, we can play with actors and actions:

# test for the new method
class MixedColumnOrdererTest < Minitest::Test
  def test_order
    Random.srand(1)
    input = [["a1", "a2"], ["b1", "b2"], ["c1", "c2"], ["d1", "d2"], ["e1", "e2"]]
    expected = [["c1", "a2"], ["b1", "c2"], ["e1", "e2"], ["a1", "d2"], ["d1", "b2"]]
    assert_equal expected, MixedColumnOrderer.new.order(input)
    Random.srand
  end
end

# code

class Phrases
    DATA =
    [ ["the horse and the hound and the horn", "that belonged to"],
      ["the farmer sowing his corn", "that kept"],
      ["the rooster that crowed in the morn", "that woke"],
      ["the priest all shaven and shorn", "that married"],
      ["the man all tattered and torn", "that kissed"],
      ["the maiden all forlorn", "that milked"],
      ["the cow with the crumpled horn", "that tossed"],
      ["the dog", "that worried"],
      ["the cat", "that killed"],
      ["the rat", "that ate"],
      ["the malt", "that lay in"],
      ["the house", "that Jack built"]
    ]

  attr_reader :data

  def initialize(orderer: OriginalOrderer.new)
    @data = orderer.order(DATA)
  end

  def phrase(num)
    data.last(num).join(" ")
  end

  def size
    data.size
  end
end

class House
  attr_reader :phrases, :prefix

  def initialize(phrases: Phrases.new, prefixer: MundanePrefixer.new)
    @phrases = phrases
    @prefix = prefixer.prefix
  end

  def recite
    1.upto(phrases.size).collect {|i| line(i)}.join("\n")
  end

  def phrase(num)
    phrases.phrase(num)
  end

  def line(num)
    "#{prefix} #{phrase(num)}.\n"
  end
end

class MixedColumnOrderer
  def order(data)
    data.transpose.map { |column| column.shuffle }.transpose
  end
end

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

class OriginalOrderer
  def order(data)
    data
  end
end

class RandomLastOrderer
  def order(data)
    data[0..-2].shuffle << data.last
  end
end

class PiratePrefixer
  def prefix
    "Thar be"
  end
end

class MundanePrefixer
  def prefix
    "This is"
  end
end

puts
phrases = Phrases.new(orderer: MixedColumnOrderer.new)
puts House.new(phrases: phrases).line(12)

This requirement, that we mix up actors and actions, gave us the impetus to extract a Phrases class, and that class is now responsible not only for the data but for putting the data in order. House gets injected with an instance of Phrases, and that Phrases object is responsible for returning each phrase to the House class.

All of this code is super simple, and part of that simplicity comes from the fact that we’ve insisted on finding straightforward abstractions.

Also, we’re using composition, and we were able to conceive of writing a new orderer to do this, and we were able to think about extracting the data off into some other place where the array of data could have some different kind of shape.

Loosen Coupling in Tests

This block we loose coupling between objects, we started by writing thest first.

A few comments on how we extracted DATA from House and created Phrases class, however we don’t have test in place, and if we look carefully the arrays with the phrases kind of belong to House and also the logix which is independent of what data we might have.

Phrases isn’t about House at all, it’s about assembling a bunch of little bits into a longer phrase. This is because how the code is designed.

Test that was intention revealing that will force us to loose coupling between House and Phrases:

class PhrasesTest < Minitest::Test
  def test_phrase
    expected   = "?" # The House data makes a confusing test due to bad code design
    assert_equal expected, Phrases.new.phrase(3)
  end
end

The first thing we’re going to do is go over to the code and do a tiny refactoring where we extract and then inject the data in order to create a seam where we could inject something else.

  HOUSE_DATA =
  [ ["the horse and the hound and the horn", "that belonged to"],
    ["the farmer sowing his corn", "that kept"],
    ["the rooster that crowed in the morn", "that woke"],
    ["the priest all shaven and shorn", "that married"],
    ["the man all tattered and torn", "that kissed"],
    ["the maiden all forlorn", "that milked"],
    ["the cow with the crumpled horn", "that tossed"],
    ["the dog", "that worried"],
    ["the cat", "that killed"],
    ["the rat", "that ate"],
    ["the malt", "that lay in"],
    ["the house", "that Jack built"]
  ]

class Phrases
  attr_reader :data

  def initialize(orderer: OriginalOrderer.new, input_data: HOUSE_DATA)
    @data = orderer.order(input_data)
  end

  def phrase(num)
    data.last(num).join(" ")
  end

  def size
    data.size
  end
end

With these changes we can now make more test more useful

class PhrasesTest < Minitest::Test
  def test_phrase
    input_data   = [["a1", "a2"], ["b1", "b2"], ["c1", "c2"], ["d1", "d2"], ["e1", "e2"]]
    expected = "phrase 2 phrase 3 phrase 4"
    assert_equal expected, Phrases.new(input_data: input_data).phrase(3)
  end
end

This will release Phrases to be used in different contexts, not just House

We solved this tight coupling as we always do it, by extracting some behavior and then injecting it, to create a seam that we can use to inject something else.

Another improvement is: show the readers of this new test that Phrase can handle both a one dimensional and a two-dimensional array. Let’s rename it, copy it, and create an almost identical one, but that uses a two-dimensional array as an input.

Here is the renaming result and also the Phrases#size tests:

class PhrasesTest < Minitest::Test
  def test_1d_phrase
    input_data   = ["phrase 1", "phrase 2", "phrase 3", "phrase 4"]
    expected = "phrase 2 phrase 3 phrase 4"
    assert_equal expected, Phrases.new(input_data: input_data).phrase(3)
  end

  def test_2d_phrase
    input_data   = [["phrase 1a", "1b"], ["phrase 2a", "2b"], ["phrase 3a", "3b"], ["phrase 4a", "4b"]]
    expected = "phrase 2a 2b phrase 3a 3b phrase 4a 4b"
    assert_equal expected, Phrases.new(input_data: input_data).phrase(3)
  end

  def test_size
    assert_equal 10, Phrases.new(input_data: ["a"] * 10).size
  end
end

Now, let’s turno to House class, if you look at it attentively you’ll see that it has nothing to do with House anymore, it takes in some bits and assembles them.

Same procedure: Copy/Paste class => replace old_name with new_name => run tests

class CumulativeTale
  attr_reader :phrases, :prefix

  def initialize(phrases: Phrases.new, prefixer: MundanePrefixer.new)
    @phrases = phrases
    @prefix = prefixer.prefix
  end

  def recite
    1.upto(phrases.size).collect {|i| line(i)}.join("\n")
  end

  def phrase(num)
    phrases.phrase(num)
  end

  def line(num)
    "#{prefix} #{phrase(num)}.\n"
  end
end

With these new changes we can adjust the old tests that test every individual line so that they get assembled correctly in all those lines. Now, it’s not necessary to write all these tests of individual variants. If one works, they’ll all work.

class CumulativeTaleTest < Minitest::Test

  def setup
    @data = [["phrase 1a", "1b"], ["phrase 2a", "2b"], ["phrase 3a", "3b"], ["phrase 4a", "4b"]]
    @phrases = Phrases.new(input_data: @data)
  end

  def test_line
    expected = "This is phrase 2a 2b phrase 3a 3b phrase 4a 4b.\n"
    assert_equal expected, CumulativeTale.new(phrases: @phrases).line(3)
  end
  def test_recite
    expected =
      "This is phrase 4a 4b.\n" +
      "\n" +
      "This is phrase 3a 3b phrase 4a 4b.\n" +
      "\n" +
      "This is phrase 2a 2b phrase 3a 3b phrase 4a 4b.\n" +
      "\n" +
      "This is phrase 1a 1b phrase 2a 2b phrase 3a 3b phrase 4a 4b.\n"
    assert_equal expected, CumulativeTale.new(phrases: @phrases).recite
  end
end

We’ve created tests for the Phrases class, and we’ve renamed the House class to CumulativeTale, and dramatically simplified its tests.

Glory in One Final Easy Change

In this block we’re asked to fulfill one final requirement, add another separator to the DATA. It should be actors, actions and modifier and mix all of those.

Example: [“the rooster that crowed in the morn”, “that woke”]

“the rooster” = actor “that crowed in the morn” = modifier “that woke” = action

This change will be super easy, there won’t need much code and that’s because of how the code is currently arranged. Because we relied on abstractions rather than concretions, and made a MixedColumnOrderer instead of creating actor and action variables in the original House class.

See the .flatten.compact we had to add in order to deal with nil elements

HOUSE_DATA =
[ ["the horse and the hound and the horn", nil, "that belonged to"],
  ["the farmer", "sowing his corn", "that kept"],
  ["the rooster", "that crowed in the morn", "that woke"],
  ["the priest", "all shaven and shorn", "that married"],
  ["the man", "all tattered and torn", "that kissed"],
  ["the maiden", "all forlorn", "that milked"],
  ["the cow", "with the crumpled horn", "that tossed"],
  ["the dog", nil, "that worried"],
  ["the cat", nil, "that killed"],
  ["the rat", nil, "that ate"],
  ["the malt", nil, "that lay in"],
  ["the house", nil, "that Jack built"]]

class Phrases
    attr_reader :data

    def initialize(orderer: OriginalOrderer.new, input_data: HOUSE_DATA)
      @data = orderer.order(input_data)
    end

    def phrase(num)
      data.last(num).flatten.compact.join(" ")
    end

    def size
      data.size
    end
end

We just enhanced the behavior of CumulativeTale so that it can deal with arrays that have bits of three parts, even if some of those parts include nil.

Nothing is Something

These are the notes I took on Sandi’s talk ‘Nothing is Something’

  • Talk is comprised of four parts.

  • Sandi comes from SmallTalk land where you have 6 keywords no confitional if therefore you are force to always think and take advatange of Objects.

  • nil is something. when you send a message to a nil it will complain with undefined method for nill

# example of falling into sending messages to nil
ids = ["pig", "", "sheep"]

animals = ids.map { |id| Animal.find(id) }

animals.each { |animal| puts animal.name }
=> pig
=> NoMethodError undefined method '+' for nil

Can be midly fixed with conditionals however we are not using OOP:

# example using if conditionals
ids = ["pig", "", "sheep"]

animals = ids.map { |id| Animal.find(id) }

animals.each { |animal| puts animal.nil? "no animal" : animal.name }
=> "pig"
=> "no animal"
=> "sheep"

The solution fully leveraging OOP - Null object pattern

# example using OOP
class GuaranteedAnimal
  def self.find(id)
    Animal.find(id) || MissingAnimal.new
  end
end

class Animal
  def name
    ...
  end
end

class MissingAnimal
  def name
    "no animal"
  end
end

ids = ["pig", "", "sheep"]

animals = ids.map { |id| GuaranteedAnimal.find(id) }

animals.each { |animal| puts animal.name }
=> "pig"
=> "no animal"
=> "sheep"

☝️ Make objects that stand in for those nils

  • Inheritance is for specialization, it’s not for sharing code.

  • Composition, inject an object to play a the role of the things that varies

1) isolate the thing that varies

2) name the concept

3) define a role

4) inject the player

  • composition + dependency injection = Object Oriented Design