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.