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