Date: September 13, 2025
This blog post consists in three parts:
Watch 1: Making Sense of Conditionals
This block contains two videos. One delves into conditionals and the other explores object-oriented polymorphism.
We still have not the code to be Open. Here, we are going to pick another code smell.
The most repeated pattern are the conditional, with 2 branches and all checking number == 1 or number == 0.
The problem with repeating conditionals is evident. How many methods would you have to change to fulfill the six-pack requirement? You have to touch a bunch of places in here because the six-pack-ness is spread out over a number of these conditionals.
These conditionals provide us with generalizations in the false branches and specializations in the true branches.
If we had mixed up our styles of code, it would have been much more difficult to tell here what we needed to do next. Consistency of style enables future refactorings and enhances understanding.
Examples of bad usage of inheritance:
# BAD: Deep/Wide Inheritance Hierarchy
class Employee; end
class FullTimeEmployee < Employee; end
class PartTimeEmployee < Employee; end
class ContractEmployee < Employee; end
class SeniorFullTimeEmployee < FullTimeEmployee; end
class JuniorFullTimeEmployee < FullTimeEmployee; end
# ... gets messy and hard to understand
# BAD: Tiny Specializations Leading to Cross-Cutting Problems
class Employee
def swag_amount
100
end
def tax_rate
0.20
end
end
class LongTermEmployee < Employee
def swag_amount
super * 2 # specializes only swag
end
end
class OutOfStateEmployee < Employee
def tax_rate
0.15 # specializes only taxes
end
end
# Problem: Need LongTermOutOfStateEmployee? Can't inherit from both!
# GOOD: Use Composition for Cross-Cutting Concerns
class Employee
def initialize(swag_policy:, tax_policy:)
@swag_policy = swag_policy
@tax_policy = tax_policy
end
def swag_amount
@swag_policy.calculate
end
def tax_rate
@tax_policy.rate
end
end
class StandardSwagPolicy
def calculate
100
end
end
class LongTermSwagPolicy
def calculate
200
end
end
class InStateTaxPolicy
def rate
0.20
end
end
class OutOfStateTaxPolicy
def rate
0.15
end
end
# Now you can mix and match any combination:
Employee.new(
swag_policy: LongTermSwagPolicy.new,
tax_policy: OutOfStateTaxPolicy.new
)
# ACCEPTABLE: Inheritance for Leaf Nodes (Small, Focused Classes)
class BottleNumber
def container
"bottles"
end
end
class BottleNumber1 < BottleNumber
def container
"bottle" # specializes most of the small class
end
end
class BottleNumber0 < BottleNumber
def quantity
"no more" # specializes most of the small class
end
end
Watch 2: Replacing Conditionals with Polymorphism
“Poly” means many, “morph” in this case means form. Many forms.
We use polymorphism to describe a situation in which many different objects can respond to the same message.
This means that the message sender doesn’t know or care the type of the object that it gets passed, and it means that later you can create new objects that polymorphously play some existing role and get that behavior into your app without having to change anything about those objects that are sending the messages.
Let’s implement a Factory object model that evaluates and creates depending on the request, different new objects, these new objects will inherit from a general BottleNumber and will contain specializations. This will reduce the conditionals we have into several new small classes.
This change is like primitive obsession but instead of obsessing upon the entire class, we actually obsess on instances of the class (0 and 1).
class Bottles
def song
verses(99, 0)
end
def verses(upper, lower)
upper.downto(lower).map { |i| verse(i) }.join("\n")
end
def verse(number)
bottle_number = bottle_number_for(number)
next_bottle_number = bottle_number_for(bottle_number.successor)
"#{bottle_number} of beer on the wall, ".capitalize +
"#{bottle_number} of beer.\n" +
"#{bottle_number.action}, " +
"#{next_bottle_number} of beer on the wall.\n"
end
# Factory
def bottle_number_for(number)
case number
when 0
BottleNumber0
when 1
BottleNumber1
else
BottleNumber
end.new(number)
end
end
class BottleNumber
attr_reader :number
def initialize(number)
@number = number
end
def to_s
"#{quantity} #{container}"
end
def quantity
number.to_s
end
def container
"bottles"
end
def action
"Take #{pronoun} down and pass it around"
end
def pronoun
"one"
end
def successor
number - 1
end
end
class BottleNumber0 < BottleNumber
def quantity
"no more"
end
def action
"Go to the store and buy some more"
end
def successor
99
end
end
class BottleNumber1 < BottleNumber
def container
"bottle"
end
def pronoun
"it"
end
end
Quiz:
This refactoring is super easy, yet it greatly increases the abstraction of the code. List some qualities of the current code that make this refactoring so easy.
All of the following are true:
Imagine the code you would have written to solve the 99 bottles problem before you knew about Shameless Green or read the 99 Bottles of OOP book. (You may actually have made a stab at solving the problem before starting the course–that’s the code I’m asking about here.) Would it have been equally easy to refactor that code into this state? If not, what qualities of the code would have made the refactoring difficult?
It’s common for that first attempt to have the following qualities:
Watch 1: Transitioning Between Types
Here, we are tackling the Liskov violation that .succesor represents, remember it should return BottleNumber not a number.
We named the factory "self.for". The name ".for" implies that it takes an argument, but in my experience factories always do. The job of a factory is to pick an object, and it needs some thing to pick on.
class Bottles
def song
verses(99, 0)
end
def verses(upper, lower)
upper.downto(lower).map { |i| verse(i) }.join("\n")
end
def verse(number)
bottle_number = BottleNumber.for(number)
"#{bottle_number} of beer on the wall, ".capitalize +
"#{bottle_number} of beer.\n" +
"#{bottle_number.action}, " +
"#{bottle_number.successor} of beer on the wall.\n"
end
end
class BottleNumber
attr_reader :number
def initialize(number)
@number = number
end
def self.for(number)
case number
when 0
BottleNumber0
when 1
BottleNumber1
else
BottleNumber
end.new(number)
end
def to_s
"#{quantity} #{container}"
end
def quantity
number.to_s
end
def container
"bottles"
end
def action
"Take #{pronoun} down and pass it around"
end
def pronoun
"one"
end
def successor
BottleNumber.for(number - 1)
end
end
class BottleNumber0 < BottleNumber
def quantity
"no more"
end
def action
"Go to the store and buy some more"
end
def successor
BottleNumber.for(99)
end
end
class BottleNumber1 < BottleNumber
def container
"bottle"
end
def pronoun
"it"
end
end
Quiz
What is the official definition of the Liskov Substitution Principle?
Or more generally…
If ‘official’ Liskov has to do with inheritance, how can whatever’s wrong with the
#successormethods be a Liskov violation? Write a new Liskov Substitution Principle definition that extends it to cover both problems.
The Liskov Substitution Principle can be expanded to include not only subtypes but also any other object that is meant to be interchangeable with current one. Liskov requires that methods return the objects that they “promise” to return.
In our case the very name of the #successor method implies that it returns something that acts like a BottleNumber, that is, that it returns an object that conforms to the API of the receiver. Returning a number instead of a BottleNumber forces that caller to check the returned object’s type in order to know how to talk to it. This is a Liskov violation.
Watch 1: Making the Easy Change
In this lesson we discovered that the code is now open to the six-pack requirement.
A quick recap of what we’ve done so far:
We started out by writing the Shameless Green implementation of 99 Bottles.
then we got a new requirement to say 1 six-pack instead of 6 bottles.
We decided to make this code open to change (we didn’t really know how to do that)
Therefore we used that flow chart that suggested that we find code smells (they are 24).
We identifed code smells, and then we picked the code smell that we thought, if we corrected it (duplicate code).
We had to isolate the things we want to vary.
The first code smell that we worked on was all that duplication in the verse method; we decided it contained concealed concepts, and we used the flocking rules to identify those concepts, to give those concepts names.
Selecting the most similar pieces of code
Finding the smallest difference between them
Making the simplest change to remove that difference
After that, we decided that all the methods that we’d created using the flocking rules were obsessing upon a primitive.
We extracted the BottleNumber class in order to cure that obsession.
Finally, we noticed that we had repeating conditionals in that BottleNumber class, and we cured those conditionals by following the Replace Conditional with Polymorphism recipe and making a little inheritance hierarchy of different players of the BottleNumber role.
For implementing the six-pack we switched to TDD so the first thing to do is writing a failing test.
We changed in bottles_test.rb the following, notice the “1 six-pack”
7 bottles of beer on the wall, 7 bottles of beer.
Take one down and pass it around, 1 six-pack of beer on the wall.
1 six-pack of beer on the wall, 1 six-pack of beer.
Take one down and pass it around, 5 bottles of beer on the wall.
Then ran tests and failed:
7 bottles of beer on the wall, 7 bottles of beer.
-Take one down and pass it around, 1 six-pack of beer on the wall.
+Take one down and pass it around, 6 bottles of beer on the wall.
-1 six-pack of beer on the wall, 1 six-pack of beer.
+6 bottles of beer on the wall, 6 bottles of beer.
Take one down and pass it around, 5 bottles of beer on the wall.
Then we added a new class of specialization BottleNumber6 with two methods #quantity and #container and don’t forget about the Factory, we gotta add one case branch.
class BottleNumber
attr_reader :number
def initialize(number)
@number = number
end
def self.for(number)
case number
when 0
BottleNumber0
when 1
BottleNumber1
when 6
BottleNumber6
else
BottleNumber
end.new(number)
end
...
class BottleNumber6 < BottleNumber
def quantity
"1"
end
def container
"six-pack"
end
end
“Make the change easy, (warning, this might be hard), then make the easy change.”, and that’s exactly the experience that we had. We’ve spent chapters making the code open, but once it was open, the change was easy.
If you learn the code smells and you get familiar with even just the names of the refactorings, you can identify code smells and look up the refactoring recipes when you want to use them.
It’ll make your code better and your life easier.
Watch 2: Defending the Domain
It’s true that the number of bugs in code is related to the volume of code, the more code you write, the more bugs you’ll have, so it’s better to minimize the amount of code you have.
However, it’s not a good idea to reduce it beyond the logical point. It’s only a good idea to reduce it if it’s correct.
The original class, Bottles, can know about BottleNumber. It has to know about how to get them but once we extract those BottleNumber# classes we have to turn our back on Bottles.
Bottles can know about the things that it’s gonna contain, BottleNumber can’t know about the objects that will contain it. The relationship can’t be bidirectional.
It’d mean you can never use them in another context
The above ideas were to stand your domain on the next proposal:
class BottleNumber6 < BottleNumber
def quantity
"1"
end
def container
"six-pack"
end
end
# VS
class BottleNumber6 < BottleNumber
def to_s
"#{quantity} #{container}"
end
end
Sandi’s summary
The purpose of this block was to produce a code arrangement that was open to the
six-packrequirement. Not only did it succeed in fulfilling that requirement, but along the way it also resolved a number of other issues.
This block explored the
Data Clumpcode smell. It replaced aSwitch Statementwith a set ofpolymorphic objects, which it created using afactory. It corrected theLiskov violationinsuccessor, and used that problem as a jumping-off point for a more general lesson about how to change the return types of polymorphic methods.
The
BottleNumberfor factory was straightforward and most certainly did the job. While simple factories like this work great in many situations, they’re not best for every case. There’s a whole world of different styles of factories waiting to be explored.