Over the years, I’ve accumulated a set of three principles that help me write code that’s more modular, readable, and modifiable. I apply them when writing new code to try to make things easier for my colleagues, and I use them when refactoring existing code that’s gotten too complex to allow tacking on one more edge case.
Over time, I’ve recognized that many heuristics I use for code quality are just specializations of these principles. Here they are in one place.
Write code at a single level of abstraction
Avoid writing code that is a mix of high and low level concepts. Instead, write top-level methods entirely in terms of other methods, that themselves do the work. These will often be private methods. This style of writing code will often lead iceberg files with 90% of the lines being in the private section and just a bit showing in the public section.
That way the reader of the high-level method can focus on what the method does rather than how. This makes code easier to read.
This style is particularly helpful when writing system/feature/acceptance tests.
I also heavily use this style when writing view code in Elm. Ideally readers of my top-level view
function don’t need to care about the specifics of my rendering layer (HTML? Canvas?) and instead focus on what UI elements are present (e.g. a loading screen, main content, a summary, etc). Consider the following code to render a survey. All the pieces are written at a high level.
survey
[ surveySection "Toppings"
[ yesNoQuestion "Pineaple on Pizza?" PoP "pop"
, yesNoQuestion "Anchovies?" Anchovies "ancho"
]
, surveySection "Crust"
[ pickOne
[ ("Thin Crust", Thin)
, ("Thick Crust", Thick)
, ("Chicago Style", Chicago)
]
]
]
Push conditionals up the decision tree
As programmers, we often like to write code that attempts to follow a single-ish main path through our code. To do this, we try to have side-branches either terminate quickly, or merge back into the main path. This leads to duplicated checks, deeply nested if/else, and unnecessarily complex conditional code.
Instead, pushing conditions up the decision tree means that you have to handle much of the uncertainy and edge cases of your code at the boundaries of your program. This leads to much more confident code.
For the remaining conditions, branching early forces us to be honest about the fact that there is more than one main path through our program. Flat conditionals at the top of your decision tree are much easier to read, reason about, and debug.
Considier the following complex conditional from the noisy animal kata:
# BEFORE
def make_noise(loud: true)
if is_bird && !loud
make_bird_noise(false)
end
if loud
if is_mammal
2.times { puts mammal_noise }
end
make_bird_noise(true) if is_bird
elsif is_mammal
puts mammal_noise
end
end
Applying this principle removes a lot of the accidental complexity in the method. Now we can easily see how logic flows. It is also much easier to scan the method and see some other inconsistencies and refactors we could apply.
# AFTER
def make_noise(loud: true)
if is_bird && loud
make_bird_noise(true)
elsif is_bird && !loud
make_bird_noise(false)
elsif is_mammal && loud
2.times { puts mammal_noise }
elsif is_mammal && !loud
puts mammal_noise
end
end
This principle has come in particularly clutch for me when refactoring gnarly multi-step form code, both on the front-end and back-end.
Separate branching from doing code
This principle says that code that branches doesn’t get to have implementation details. If a method contains a conditional, each branch may only call out to another method. This guideline and the Sandi Metz rule of max 5 lines per method naturally lead to a similar place by minimizing how much you can fit in an if/else
expression.
This principle emphasizes compositionality and reuse by breaking apart your algorithm at its natural joints.
I find myself applying specialized variants of this rule all the time, such as my recommendation to separate expensive calculations and the memoization of those calculations to separate methods, or my recommendation to separate code that checks for presence from code that calculates values when working with Maybe
in Elm.
Consider the following smelly code:
# SMELLY
def save(for_real:)
if for_real
File.open("#{@title.downcase}.txt", "w") do |file|
file.puts @title
file.puts @body
end
else
$stdout.puts "PREVIEW"
$stdout.puts @title
$stdout.puts @body.slice(0, 240)
end
end
now compare with the alternative that applies the principle:
# BETTER
def save(for_real:)
if for_real
save_to_file
else
output_preview
end
end
Now it’s easier to remix the various parts of the method. If someone knows they want to save to a file for sure, they don’t need to hardcode a boolean save(for_real: true)
(thats a form of control coupling), instead they call the path they want to use directly as save_to_file
.
If you plan on having sub-classes, particularly if you are using a template method pattern, this style decomposes your method in a natural way that provides most of the necessary hooks for sub-classes to override.
Triangle of separation
You may have been reading some of the examples and thought to yourself “this feels like it would have been a better example for one of the other principles”. That’s because all of of the principles converge on the same kind of code. They are 3 sides of a coin, err… triangle?
It’s the same idea viewed from a few different angles. I’ve nicknamed this trio the triangle of separation, because it’s all about giving you heuristics for discovering seams for separating code.
Taken together, these ideas help you write code that’s easier to read, easier to change, easier to extend, and easier to remix.