Today’s issue is a continuation of control-flow refactoring. We will explore an example of nullable return values, how we can refactor them and what it looks like if we go too far and make it silly.
Recap on Part 1—Loops
I invite you to take a look into our previous issue on this topic where we explored a similar code smell on Loops and pull vs. pull iteration. The chapter on code smells applies to today’s issue as well.
Why should I care? Isn’t this all… simple?
If statements are by far the most popular control structure. That makes it the primary contributor to indentation and cognitive load when it comes to reading code. In fact, most code smells and complexity are named after confusing branching logic.
Long method
Nested branching logic
High cyclomatic complexity
Ignoring Exceptions
Nested Try Catch blocks
Jump code spaghetti
You can master all these complexities and write simple code. The tricky part is understanding what causes the complexity to be written and how to refactor complex branching logic into simple ones. We will start of by exploring the topology of if statements and their derivative imperative (Object-oriented or procedural) use cases.
What’s in an If statement?
At its core it seems all very simple—though we quickly end up in goto statement hell if you look at the pure assembler instructions. Every jump in our CPU instructions wants to be an if statement when they grow up. Every jump is hiding an if statement. Every if statement is hiding a jump.
Each if statement can have these four elements:
// I can’t decide… 🙊
if statement // 1 - the main condition
// then do this 🐒
else if statement // 2 - the secondary condition, optional
// changed my mind, do this! 🙉
else // 3 - the default / “otherwise” condition
// ok nevermind! 🙈
endif // 4 - closing of to continue flow
// where were we? 🐵
At this machine level we also get to experience the primary problems with jumpy code: context. You see— the if statements retain the stack and closure context of the environment they were invoked from. Which means you have to keep them in your head as you are traversing 2-3 paths simultaneously with your thought-process.
It’s like juggling. Go deeper one more if, 3 paths become 6. Another else-if and an optional level and they become 14. This is incredibly difficult to reason about for even the most seasoned developer.
And don’t be mistaken—seasoned developers do write messy code like this, just like junior (from the Gilded rose Kata in TypeScript):
updateQuality() {
for (let i = 0; i < this.items.length; i++) {
if (this.items[i].name != 'Aged Brie' && this.items[i].name != 'Backstage passes to a TAFKAL80ETC concert') {
if (this.items[i].quality > 0) {
if (this.items[i].name != 'Sulfuras, Hand of Ragnaros') {
this.items[i].quality = this.items[i].quality - 1
}
}
} else {
if (this.items[i].quality < 50) {
this.items[i].quality = this.items[i].quality + 1
if (this.items[i].name == 'Backstage passes to a
TAFKAL80ETC concert') {
if (this.items[i].sellIn < 11) {
if (this.items[i].quality < 50) {
this.items[i].quality = this.items[i].quality
+ 1
}
}
if (this.items[i].sellIn < 6) {
if (this.items[i].quality < 50) {
this.items[i].quality = this.items[i].quality + 1
}
}
}
}
}
if (this.items[i].name != 'Sulfuras, Hand of Ragnaros') {
this.items[i].sellIn = this.items[i].sellIn - 1;
}
…
But what expresses their maturity is that they do not leave it in this state after it is functionally working. This process is called refactoring.
DDD - Invariants
The trickiest types of if statements are those that produce and guard against over-the-wire invariants. You try to create a new charge on Stripe. You except a Charge object or at least a Transaction back.
You get an error message instead.
What do you do?
Guard blocks are especially tricky as they are commonly confused with defensive programming. You know, this kind of programming:
Psst…! There’s a whole separate CTT issue on DDD Invariants and Aggregates:
Polymorphism
“Pomylohwat?!”
- Senior 10yoe Rust Programmer, 2023
Object oriented paradigms have set out to address the problem of cyclomatic complexity and jumpy code with a set of rules where we put the jumpy behavior very close to the data it makes decisions from in order to minimise the size of the jump. By minimising the jump we also have to carry over less information to the new context.
Think of it with the strategy pattern using class and method extraction. Rather than having the branches expressed in our immediate calling code—we move each branch to a separate method or class.
// Wraps the previous 1-2-3 branches into a closure or object
const pathForward = rightPathForward(condition);
return pathForward.call(context); // Continue Step 4
The Core Problem—Responsibility
The most common your team of engineers are facing is handling Nullable return values. Some languages promote these to Maybes, Options, Futures and question-mark types. Some outright have no concept of null or nil.
Let’s take an example of finding and applying a discount code upon checkout in a typical e-commerce web application.
The intention of a clean coder isn’t to write some exotic code without using the if control construct. And then annoy the entire team about this new religion.
Our goal is to ensure that all if statements are hidden from the code preceding the outer-most logical structure. In the case above, within the checkout function.
Alas, that doesn’t solve the problem completely. If you open the checkout function you may encounter the mess that you hid away from the public API.
So we may take another step within the refactored code to factor it further:
But be cautious—taken to the extreme you will end up with a new Class for every line of code written. This is a common criticism of Clean Code applied clumsily—not weighing in when to stop.
However, you may notice that with Option 2 the code is linear once more where with Option 1 it isn’t. That is our goal with Clean Code. Regardless of your chosen favorite design pattern—we aim to truncate the amount of control flow logic that lives at the top-most levels.
If you find your team is applying Design Patterns everywhere but isn’t reducing the control flow mess—it may be time to show them this article.