Date: October 11, 2025
This blog post consists in two parts:
This block contains an example that illustrates how convenient inheritance can be, and how it can go very badly wrong.
In this section we’re going to create a beautiful, simple, little inheritance hierarchy to show how useful inheritance can be to quickly getting things done, and then we’re going to introduce a change in requirements that makes everything go horribly wrong to show when you should switch from inheritance to composition.
The new requirement is a variant of House that mixes up the bits, randomly. We need the original requirement to still work, House should still work just like it does, but they also want another variant where the lines come out mixed up, randomly, every time.
Let’s do that with inheritance.
First proposal:
# original code
class 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"]
def recite
1.upto(12).collect {|i| line(i)}.join("\n")
end
def phrase(num)
data.last(num).join(" ")
end
def line(num)
"This is #{phrase(num)}.\n"
end
def data
DATA
end
end
# RandomHouse code
class RandomHouse < House
def data
@data ||= super.shuffle
end
end
# PirateHouse code
class PirateHouse < House
def prefix
"Thar be"
end
end
puts
puts RandomHouse.new.line(12)
puts
puts PirateHouse.new.line(12)
New requirement is going to make everything break badly with this inheritance approach. Implement RandomPirateHouse without duplicating code. Module is not feasable, composition is the right solution.
You want your inheritance hierarchies to be shallow, not very deep, and narrow, not very wide. You want the subclasses to really be a kind of thing of the superclasses.
Use of inheritance only on your object dependency graph leaf nodes. t the core of your app are key objects that depend on others, forming a chain outward. At the edges are leaf objects like BottleNumber# that are depended on but depend on nothing else.
When you create subclasses that are that far out away from the center of your domain they’re probably not all that important, and they’re probably not going to change that much, and it might not go badly wrong to use inheritance out there.
In the last section we implemented a little inheritance hierarchy that worked however once we wanted to introduce a new change it flopped.
First proposal:
class 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"]
attr_reader :data
def initialize(random=false)
@data =
if random == true
DATA.shuffle
else
DATA
end
end
def recite
1.upto(12).collect {|i| line(i)}.join("\n")
end
def phrase(num)
data.last(num).join(" ")
end
def line(num)
"#{prefix} #{phrase(num)}.\n"
end
def prefix
"This is"
end
end
puts
# this code looks weird
puts House.new(true).line(12)
In object-oriented programming, we can expect that if an object knows enough to pass you a true or a false, or a truck or a carrot, they know enough to pass you a smarter thing.
You shouldn’t have to look at the type and supply behavior for different kinds of things. You should just have a thing that came in that you can send a message to, and get the thing back that you want.
We don’t like this code at all, but the good thing about writing this conditional is, it tells us exactly what we wish we had.
We need an object to stand in for true that behaves like what’s in the true branch, and we need another object to stand in for false that returns what’s in the false branch. Those two objects play a common role. They have to conform to the same API, and in the methods that conform to that API they have different behavior so that they can be treated interchangeably, but they do different things.
We know we need two classes, and we know they have to conform to the same API, but we don’t want to have to name them right now.
Second proposal:
class 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"]
attr_reader :data
def initialize(orderer: OriginalOrderer.new)
@data = orderer.order(DATA)
end
def recite
1.upto(12).collect {|i| line(i)}.join("\n")
end
def phrase(num)
data.last(num).join(" ")
end
def line(num)
"#{prefix} #{phrase(num)}.\n"
end
def prefix
"This is"
end
end
class RandomOrderer
def order(data)
data.shuffle
end
end
class OriginalOrderer
def order(data)
data.shuffle
end
end
# we can now invoke RandomOrderer and PiratePrefixer since we rely on composition
puts House.new(orderer: RandomOrderer.new).line(12)
puts
puts House.new(prefixer: PiratePrefixer.new).line(12)
We should name objects after we have written down the code, remember Objects are, not doers.
And for the new requirement that entailed RandomPirateHouse we can do the follwing:
class 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"]
attr_reader :data, :prefix
def initialize(orderer: OriginalOrderer.new, prefixer: MundanePrefixer.new)
@data = orderer.order(DATA)
@prefix = prefixer.prefix
end
...
class PiratePrefixer
def prefix
"Thar be"
end
end
class MundanePrefixer
def prefix
"This is"
end
end
puts
puts House.new(orderer: RandomOrderer.new, prefixer: PiratePrefixer.new).line(12)
Here is another variant we want to tackle and is regarding the ending of the sentences.
class RandomButLastOrderer
def order(data)
data[0..-2].shuffle << data.last
end
end
Conclusion: Sandi does use inheritance a lot. It can be super easy, and handy, but as soon as it starts going wrong, as soon as you start creating deep or wide hierarchies, or subclasses that duplicate behavior across the hierarchy, you have to switch to composition.
Composition is a little bit harder for humans to think about, but once you get it, once you get the knack in your head, it’s easy to use, and it’s very flexible and changeable. The key to composition is being able to imagine objects that apparently do nothing.