Date: September 06, 2025
This blog post consists in three parts:
Watch 1: Appreciating Immutability
To mutate is to change.
State is the particular conditional of something at certain point in time. and a variable is a thing that varies.
In the real world, things change state, or they vary, over time.
And it makes perfect sense that in computer programming we would choose the term variable for that thing that holds state, that changes over time. This choice reflects our expectation, that all things vary.
It’s common in OO to change the state of objects; we send a message and pass a bit of data, and then you hold on to that new bit of data.
But it’s possible, as the functional folks will gladly tell you, to write code where most objects would never have to change state. Instead of mutating an object, if it needed to vary state, you could just create a new object on that new piece of state.
What this means is if you had a coffee cup and you drank all the coffee, instead of refilling the cup, you would just get a new cup with more coffee in it.
Instead of noting that the continents had moved because of plate tectonics, you would get entirely new continents, even if they’d only moved a matter of millimeters.
What might the benefits of immutable objects be? Well, they’re easy to understand. You’re never in a situation where someone else is holding a pointer to an object that you’re also holding onto, and that object somehow changes state underneath you.
What you see is always what you get. Immutable objects are easy to reason about.
If you have mutable objects, where state is changing, often you need collaborators in the tests that you’re creating. So you have to spin up these other objects in order to get state to change in the object that’s under test.
But for immutable objects, you would just create a new object on a different piece of state.
So there are many good reasons to love immutable objects. And the question is why don’t we use them more?
The first is just habit, when you learned OO, or any example that you look at using OO, very often mutates objects.
The second reason I think is an unquestioned assumption that many people make that object creation will be expensive and that it’s better to mutate an existing object rather than create a new one.
Watch 2: Assuming Fast Enough
In the previous section made the case for immutability. And it’s a good thing. We often don’t do it though, and it’s mostly for two reasons. One: habit, we’re not in the habit of thinking about creating immutable objects. But two, a bigger issue, is even if we want to create immutable objects, very often, we imagine that it will be too expensive to create new objects.
A person named Phil Karlton famously said:
There are only two hard problems in Computer Science:
- naming and
- cache invalidation.
Cache is local copy of something. A temporary variable, when you declare a variable and say equal some operation, you’ve created a cache. Also, known as memoization.
Why would you cache? Sometimes we cache to give a name to the result of an intermediate operation, to add clarity to the code.
The other reason we may cache is to preserve the result of a costly long running operation. And so now we have a problem. If you save the result of a long running operation, how will you know when your cache gets dirty? (different result and you’re holding on to a previous cache copy)
The code to figure out that your cache is dirty is cache invalidation.
There’s a thing called the 80/20 rule, which says that 80%, or 90% or 95%, of an application’s work occurs in 20% of its code. (Most of the code don’t run pretty much)
Don’t add complexity based on guesses.
Write the simplest possible code, and then, and only then, if you find that parts of your app are too slow, (if someone complains) then profile your code to identify the slow parts and then work on them to make them faster.
A profiler is a tool that watches your code as it runs and measures how much time you spend in certain operations.
Your initial solution should avoid mutation.
You should treat object creation as free and you should also avoid caching, just rerun operations and use the results.
If you find that parts of your app are too slow and you profile it, you might find that you’d be better off to break some of your objects into smaller parts.
If you have parts that you need to mutate, or if you have parts that you need cache and invalidate, it’s nice to minimize the size of those parts.
If you can create objects for the ideas of your business and have those objects hold on to the bags of state, you end up in a situation where it’s easy to test your business logic and you’re decoupled from the database.
Example of objects as buisness ideas:
# DON'T: Business logic coupled to database/state
class Order
def initialize(id)
@id = id
end
def apply_discount
record = Database.find(@id)
if record.total > 100
record.discount = record.total * 0.10
record.save
end
end
end
# DO: Extract business logic into separate objects
class DiscountCalculator
def initialize(total)
@total = total
end
def calculate
@total > 100 ? @total * 0.10 : 0
end
end
class Order
def apply_discount
record = Database.find(@id)
discount = DiscountCalculator.new(record.total).calculate
record.discount = discount
record.save
end
end
# Now DiscountCalculator is easy to test without database:
# DiscountCalculator.new(150).calculate => 15.0
Here we applied the lessons from prior block to reduce the number of new BottleNumbers and simplify the code.
Watch 1: Creating BottleNumbers
With current code, how many instances of BottleNumber get created over the course of the song? It happens every time verse calls one of these methods, and that happens nine times in verse, that means we’re creating 900 BottleNumbers.
Too many? As we said before, only if this code is too slow. Caching these values is very complicated.
The next trial will reduce the number of BottleNumber:
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.new(number)
"#{bottle_number.quantity.capitalize} #{bottle_number.container} of beer on the wall, " +
"#{bottle_number.quantity} #{bottle_number.container} of beer.\n" +
"#{bottle_number.action}, " +
"#{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
Watch 2: Recognizing Liskov Violations
In last session we created bottle_number within verse and that reduced the BottleNumber from 900 to 200 creation. We left the 4 phrase with no implementation of this new cache since it requires a different number. We need the succersor number.
Adding next_bottle_number implies that you’re going to get another BottleNumber back, and yet here you don’t. I’m going to call this yet another violation of that generalized Liskov Substitution Principle.
The method makes a promise that you are going to get back a BottleNumber but instead you get a number.
Why? Because back in the shameless green lesson it was useful and seemed like a good idea. We didn’t even have BottleNumber.
We should have changed successor so that it would return instances of BottleNumber rather than instances of a number.
Notice that the refactoring recipes, even though they guided us all along the way, they don’t really help with this problem.
You have to be able to look at the names of things and understand that you need to make an additional change in the successor method; because you’ve changed type, successor needs to return the new type.
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.new(number)
next_bottle_number = BottleNumber.new(bottle_number.successor)
"#{bottle_number.quantity.capitalize} #{bottle_number.container} of beer on the wall, " +
"#{bottle_number.quantity} #{bottle_number.container} of beer.\n" +
"#{bottle_number.action}, " +
"#{next_bottle_number.quantity} #{next_bottle_number.container} of beer on the wall.\n"
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
Sandi’s recap until this lesson:
This block continued the quest to make Bottles open to the six-pack requirement. It recognized that many methods in Bottles obsessed on number, and undertook the Extract Class refactoring to cure this obsession. The refactoring created a new class named BottleNumber.
During the course of the refactoring, conditionals were examined from an experienced OO practitioners’ point of view. This block also explored the rewards of modeling abstractions, the trade-offs of caching, the advantages of immutability, and the benefits of deferring performance tuning.
Most programmers are happier with the current code than they were with Shameless Green, but this version is far from perfect. The total Flog score, for example, has gone up again. From Flog’s point of view, after turning one conditional into many back in previous lessons, you’ve now compounded your sins by introducing a new class which adds no new behavior but increases the length of the code.
Also, there are no unit tests for BottleNumber. It relies entirely on Bottle ‘s tests. The code still exudes many smells (duplication, conditionals, and temporary field, to name a few). And, finally, it commits a Liskov violation in the successor method.
Recent refactorings were undertaken in hopes of making the code open to the six-pack requirement, but this has not yet succeeded. You’ve been acting in faith that removing code smells would eventually lead to openness. It’s possible that your faith is being tested.
Despite the imperfections listed above, there are ways in which the code is better. There are now two classes, but each has focused responsibilities. While it’s true that the whole is bigger, each part is easy to understand and reason about.
The code is consistent and regular, and embodies an extremely stable landing point that splendidly enables the next refactoring.
Quiz for these Liskov, primitive obsession cure session:
They make a promise that they do not keep.
They return an unexpected type, forcing the receiver to know too many things (have too many dependencies).
900 before, 200 after.
FREE
Watch 1: Consolidating Data Clumps
What’s a Data Clump? Martin Fowler defines it as three or four things that always seem to come together, either passed as perimeters or used as a group.
Examples:
Imagine you had a reporting system where many methods took a starting and an ending date.
start_date and end_date are always passed together and share behaviors (validation, formatting, comparison).
class DateRange
attr_reader :start_date, :end_date
def initialize(start_date, end_date)
@start_date, @end_date = start_date, end_date
validate
end
def validate
raise "Invalid range" if start_date > end_date
end
def duration
(end_date - start_date).to_i
end
end
Instead of passing two arguments everywhere:
Report.new(start_date, end_date)
you pass a single cohesive object:
Report.new(DateRange.new(start_date, end_date))
Sandi Metz outlines an implicit 4-step reasoning process in this section:
Look for two or more values that “always travel together” passed as arguments or used side by side (quantity + container, start_date + end_date, x + y).
Ask: “Do other parts of the app perform similar checks or operations on these values?”
Example: validating a date range, computing coordinates, or pluralizing bottles.
ObjectCreate a class that encapsulates both the data and the behavior related to it. This lets scattered logic “coalesce” into one place.
Replace multiple arguments with a single object.
This reduces duplication and increases consistency, your app now speaks in terms of meaningful concepts (DateRange, Point, BottleNumber) instead of raw primitives.
This refactor simplified the verse, reduced duplication, and made the method cleaner:
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.new(number)
next_bottle_number = BottleNumber.new(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
end
class BottleNumber
attr_reader :number
def initialize(number)
@number = number
end
def to_s
"#{quantity} #{container}"
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