Date: August 09, 2025
I’ve started a 12-week walkthrough of Practical Object-Oriented Design Course by Sandi Metz, and we’re reading the first two chapters of 99 botles.
Here are the highlights from the first session:
We were tasked to watch Sandi Metz’s talk All the little things (here my notes) and then to complete as much of the coding challenge as possible and make all tests pass within 15 mins. The test was about singing the 99 bottles of beer song which is a song counting down bottles of beer.
SIDE NOTE: I asked chatGPT about this song after I solved the exercise which is wrong. My mentor has always told me to figure out as much as I can on the domain model (the rules and boundaries of the problem). And since it was the first time I knew about this song I missed very important details, for instance: knowing the highest number possible is 99 and the lowest is 0. It may have changed my approach.
The tests asserted/expected a concatenated string and at a first glance I identified that numbers and a few words were changing as we were counting down.
I tried to follow conventions like short classes, short methods and so on. First, I made sure I was outputting the correct string and then pinpointed the numbers that needed the counting-down, then came up with the method name bottle_count_phrase
because it was meant for toggling from the words bottle
, bottles
and no more bottles
(0
, 1
or > 1 (greater than 1)
respectively) and the integer change by subtracting 1.
I tried the first version and it worked, so I advanced to the next test, until I faced the word variation (“one” or “it”). After identifying the possible variables, in this case only 2, I first wrote down an if
statement, which later became a ternary operator.
With these possible paths and having the 4 tests passing (green) this was my solution:
class Bottles
def verse(characters)
song(characters)
end
def song(number_of_bottles)
"#{bottle_count_phrase(number_of_bottles)} of beer on the wall, " +
"#{bottle_count_phrase(number_of_bottles)} of beer.\n" +
"Take #{removal_pronoun(number_of_bottles -= 1)} down and pass it around, " +
"#{bottle_count_phrase(number_of_bottles)} of beer on the wall.\n"
end
def bottle_count_phrase(number_of_bottles_remaining)
if number_of_bottles_remaining == 0
"no more bottles"
elsif number_of_bottles_remaining == 1
"#{number_of_bottles_remaining} bottle"
else
"#{number_of_bottles_remaining} bottles"
end
end
def removal_pronoun(number_of_bottles)
number_of_bottles >= 1 ? "one" : "it"
end
end
After this exercise we were encouraged to see Sandi’s solution which is the following:
def song
verses(99, 0)
end
def verses(upper, lower)
upper.downto(lower).map { |i| verse(i) }.join("\n")
end
case number
when 0
"No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"
when 1
"1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"
when 2
"2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n"
else
"#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottles of beer on the wall.\n"
end
As soon as I saw it, felt like my mentor was talking to me again:
Where I’m getting with this is, that if I knew that we were only to receive integers as inputs and that the song based on that input, was always going to be the same except the words (‘one’ vs. ‘it’) and numbers, I could have hardcoded the solution (not saying it’s an easy approach to come up with) but my approach was thought with more flexible design with interpolation and conditionals but definitely, I overcomplicated the solution.
Something that I noticed in this course as in the Effective testing with RSpec by Myron Marston and Ian Dees is that the authors encourage to tackle test one by one even though it sounds obvious to avoid cluttering your terminal with errors, sometimes we want to see the full picture but their approach is very effective.
Lesson learned, read thoroughly the problem, understand the inputs, outputs, if possible make a hand-written drawing, understand what changes, what type of data structure? and lastly if the result is the expected, refine it. It also reminds me of the PEDAC method.
And we must aim as quick as possible to ‘Shameless Green’ which is a solution that’s optimized for readability without consideration for future change. We can think of it as the simplest version possible.
All the problems we have caused by writting ugly code has the same simple solution.
One piece of advice: make smaller things. Make more classes, more methods and let them know about each other as little as possible.
Metric Flog is for measuring complexity of code (Assignments, Branches, Calls). The higher the worse.
Sandi used to think that she had to understand everything in order to help with anything.
Squint test, is identifying code that can benefit of changing.
Changes in shape (nested conditionals == harder to reason about)
Changes in color, meaning code has different levels of abstractions therefore difficult to follow.
Refactoring is: change its arrangement without altering its behaviour.
To do refactoring, you must have tests.
OOP build seams between objects, they send messages each other. In prodecual code, it’s just one flow.
The first refactoring is not going to be neither smart nor clever. But that’s the whole point. The point is to get to green as quickly as possible.
Write code for one test at the time.
Finish the refactoring before going off on a tangent so keep the duplication
Duplication is far cheaper than the wrong abstraction.
If you have code that you don’t understand you have two choices: you can keep it forever or you can write test around it and refactor.
All intermediate refactoring amde the code more complex.
Open/Closed. Open for extension, closed for modification.
Object oriented design is a style guide for arranging code.
With Open/Closed in place we’d be able to add new behavior without editing existing code.
Whe you have different prefixes and common suffixes, they’re candidate to have the prefix as a Class
and the suffix as a method
.
If a purpose of an object is just forwarding messages to someone else, you must ask if it justifies its existance (it’s called middleman).
Item factory, it finds the rights object and sends a message to it. (it produces the right kind of item object and then forwards messages)
Inheritance is not evil. You want subclasses to be at the leafnode of the object graph (shallow, narrow) and subclasses use all the behaviour of the super class.
Business logic != code configuration
Ah, I see exactly the part you’re struggling with. Let me break it down for you clearly, because Sandi Metz is talking about two related ideas here: message forwarding and the “middleman” problem.
Originally, the GildedRose
class has one big method (tick
) with 43 lines of conditional logic. It checks the type of item (Normal
, Brie
, Backstage
, Sulfuras
) and applies the rules for that item.
From GildedRose
’s perspective:
quality
and days_remaining
directly.Sandi’s point: we should flip this around—instead of Gilded Rose knowing how each item works, each item should know how to “tick” itself. GildedRose
just asks “item, do your tick.”
A middleman is a class whose only purpose is to forward a message to someone else. For example:
class GildedRose
def tick_item(item_name)
item = get_item_class(item_name).new
item.tick
end
end
Here:
GildedRose
isn’t doing any real work itself; it just finds the right item class and tells it to do its tick
.Sandi says: “When an object’s only purpose is to forward messages somewhere else, you have to wonder if it justifies its existence.”
GildedRose
only forwards messages and does nothing else, maybe it’s not necessary.So GildedRose
becomes effectively an item factory: it produces the right kind of item object and then forwards messages.
By doing this:
GildedRose
.Before:
GildedRose -> lots of if statements -> manipulates every item type directly
After:
GildedRose -> finds item object -> sends :tick
item object -> handles its own rules
From GildedRose’s point of view, it doesn’t care what kind of item it is, only that it responds to
tick
. That’s duck typing in action.
Yes, a middleman is sometimes unnecessary. But here, GildedRose
:
tick
That one responsibility makes it justified. Without it, you’d have to do the mapping everywhere else in your code, which would violate DRY and clarity.
✅ TL;DR
GildedRose
just figures out which object to talk to → forwards tick
→ small object design.If you want, I can draw a small diagram showing the message flow before and after refactoring—it really makes this click visually.
Do you want me to do that?