Date: October 18, 2025
This blog post consists in four parts:
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:
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?
This is the first requirement that involves that DATA array. Is it time to isolate the DATA in a separate class?
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.
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.
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.
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