Date: August 30, 2025
This blog post consists in one part:
Watch 1: Selecting the Target Code Smell
In this lesson we have more code, more abstraction which means more levels of indirection. It’s not really more complex and mini-differences are hidden in these abstractions.
We’re midway through refactoring. Seeing the code become more complicated is normal. We can learn more in the next talk. All the little things talk.
Refactoring recipes don’t promise to make the code simpler, they promise to help you make a very planned change and if fail revert it.
The code is not open yet, and we chose the code duplication as code smell to solve and we followed flocking rules for identifying abstractions.
We need to make a decision if we continue with our current code and if we revert it and pick a different code smell.
We’ve got to ask ourselves, did the refactor we implement, isolate more the parts I want to vary? ("six-pack")
And the answer is yes, quatity(number) and container(number)
If we were to vary container or quantity now, we’re dealing with a smaller amount of code.
Sandi recommended to move on and pick another code smell.
We had to pick the next code smell and Sandi helped us with the following questions:
Do any of the methods have the same shape?
Are there any arguments that have the same name?
If you were to put the private keyword in here, where would it go?
If this were 2 classes, were would you split it?
The method that have arguments, do they depend on the argument itself or the whole class?
Almost all questions are YES.
Something that caught my attention was how the number argument started as a verse in song and verses(upper, lower) methods but in verse(number) it becomes a bottle number, which is confusing, same argument name for two different concepts.
A worse problem is when we use the different names for the same concept.
def container(number)
if number == 1
"bottle"
else
"bottles"
end
end
Let’s evaluate the container(number):
You passed it an argument, and that argument is so impaired that it does not come with its own behavior. You force it to know that it should test it for some values, and you force it to know what behavior to supply, based on the value that it tested for.
You force it into the conditional. You forced it to know the different values that it might test in the conditional. And you forced it to know the behavior that applied to each of those. This is not OO.
We should be able to call number.container at this point we cannot. The code needs a smarter number.
Watch 2: Extracting Classes
A number of methods take the same argument (number). Most of them have the same shape. They contain a conditional, they would be considered private, and they depend more on the argument that got passed than the class as a whole.
These things suggest the Primitive Obsession code smell.
The cure for Primitive Obsession is to extract a class.
We’ll “wrap a coat of behavior around a primitive,” we’re taking something simple like a number or a string and giving it a bit of intelligence.
Now, since we’re going to create a class, the first thing we do is name it. What is this thing? Example: it’s a bottle. It’s blue, 22 ounces, made of plastic, and holds water. But remember what we write in code isn’t the bottle itself. It’s a representation of all bottles.
Think of a boolean, like true, you can’t pick one up or see it. It’s not physical; it’s an idea. In Ruby, true is just a variable that points to an instance of TrueClass.
That’s the beauty of object-oriented programming: it lets you build worlds where ideas are as real as physical objects. When you start modeling concepts the same way you model things, you’ve crossed an important milestone as an OO programmer. Those hidden, abstract ideas, the ones that live in the relationships between real objects, are where the most powerful abstractions are found.
Another example: in an Event Management System, perhaps you have buyers and tickets and there’s a bunch of interactions between them. You have purchases and refunds and discounts and you could resell your tickets.
Now, all of the logic to do those actions, those operations, could exist in either ticket or buyer, but it’s better if they get modeled as their own separate things. If you do that, and put the logic in its own object, you create objects that are easier to understand, easier to change, easier to vary, and easier to test.
And since tests are just a form of reuse, a test is often the first reuse of an object. If you can test it, if it’s easy to test, it means it will be also easy to reuse.
Here is another example to illustrate what primitive obsession code smell is:
# BEFORE: Primitive Obsession
class Order
def initialize(country_code)
@country_code = country_code
end
def tax_rate
if @country_code == "US"
0.07
elsif @country_code == "MX"
0.16
else
0.20
end
end
def currency
if @country_code == "US"
"USD"
elsif @country_code == "MX"
"MXN"
else
"EUR"
end
end
end
# AFTER: Extract Class to cure Primitive Obsession
class Order
def initialize(country)
@country = country
end
def tax_rate
@country.tax_rate
end
def currency
@country.currency
end
end
class Country
attr_reader :code
def initialize(code)
@code = code
end
def tax_rate
case code
when "US" then 0.07
when "MX" then 0.16
else 0.20
end
end
def currency
case code
when "US" then "USD"
when "MX" then "MXN"
else "EUR"
end
end
end
When naming instance methods, we asserted that methods ought to be named one level of abstraction higher than what they do, but the rule is different for classes. Classes ought to be named after what they are, and therefore this new class is a BottleNumber.
The class Bottles is now free of conditionals and BottleNumber only deals with number nothing about the Song and this is how this the code we finished with during this session.
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)
"#{quantity(number).capitalize} #{container(number)} of beer on the wall, " +
"#{quantity(number)} #{container(number)} of beer.\n" +
"#{action(number)}, " +
"#{quantity(successor(number))} #{container(successor(number))} of beer on the wall.\n"
end
def quantity(number)
BottleNumber.new(number).quantity
end
def container(number)
BottleNumber.new(number).container
end
def action(number)
BottleNumber.new(number).action
end
def successor(number)
BottleNumber.new(number).successor
end
end
class BottleNumber
attr_reader :number
def initialize(number)
@number = number
end
def quantity
if number == 0
"no more"
else
number.to_s
end
end
def container
if number == 1
"bottle"
else
"bottles"
end
end
def action
if number == 0
"Go to the store and buy some more"
else
"Take #{pronoun} down and pass it around"
end
end
def pronoun
if number == 1
"it"
else
"one"
end
end
def successor
if number == 0
99
else
number - 1
end
end
end