Untangling Nested Code
• 6 min read
I don't believe in hard rules for writing code. You hear them often. Things like methods shouldn't be more than 15 lines. Methods should only have one return
statement. Indentation must be 4 spaces. Such rules are too rigid.
In the real world code is much more fluid. Adhering to these hard rules distracts us from what really matters - readability. If my focus is strictly on the number of lines or return
statements, I prevent myself from writing more readable code simply because it is a few lines "too long" or has more than one return
statement.
Many of these hard rules attempt to address nested code. Nested code is hard to follow. Physically there's more visual scanning with the eyes. Mentally, each level of nesting requires more overhead to track functionality. All of these exhaust a reader.
Nested code is mostly the result of conditionals. Since conditionals are the basis for all programming logic, we can't very well remove them. We must recognize their effect on readers and take steps to minimize this impact.
Getting back on top
To improve readability, we want to bring the code back to the top level. By nature, loops and conditionals have a nested structure. There's no way to avoid nesting within these blocks. However, we can avoid nesting beyond this structure.
Let's take a look at a few examples of nested code and practices for improving their readability.
Empty blocks
You may not believe me, but I've seen the following code more than once:
1public function handle($request, Closure $next) 2{ 3 if (env('APP_ENV') == 'development') { 4 // do nothing... 5 } else { 6 if ($request->getScheme() != 'https') { 7 URL::forceScheme('https'); 8 return redirect('https://www.example.com/' . $request->getPath()); 9 }10 }11 12 return $next($request);13}
That's right, an empty if
block. I've also seen the opposite - an empty else
block. There is no rule that an if
must be paired with an else
- at least not in any of the programming languages I've used in the past 20 years. Empty blocks are dead code, remove them.
Conditional values
Nested code blocks often return a value. When these are boolean values, there's an opportunity to condense the code block and return the condition directly.
Consider the nested code within the isEmpty
method of a Set
class:
1public function isEmpty() {2 if ($this->size === 0) {3 return true;4 } else {5 return false;6 }7}
While this method block is only 4 lines of code, it contains multiple sub-blocks. Even for such a small number of lines this is hard to read, making the code appear more complex than it really is.
By identifying the conditional return of raw boolean values we have the rare opportunity to completely remove the nested code by directly returning the condition.
1public function isEmpty() {2 return $this->size === 0;3}
Given the context of this aptly named method combined with a now one line block, we decreased its perceived complexity. Although this line may appear dense, it is nonetheless more readable than the original.
Note: Condensing conditionals can work for more data types than raw booleans. For example, you can return the condition as an integer with a simple type cast. However this rapidly increases the complexity. Many programmers try to combat this by using a ternary. But a ternary condenses the code without decreasing the complexity, making the code less readable. In these cases, a guard clause is a better alternative.
Guard clauses
Nested code is often the result of logical progression. As programmers we write out each condition until we reach a level where it's safe to perform the action.
While this flow may be ideal for execution, it's not ideal for reading. For each nested level the reader has to maintain a growing mental model.
Consider the following implementation of the add
method of a Set
class:
1public function add($item) {2 if ($item !== null) {3 if (!$this->contains($item)) {4 $this->items[] = $item;5 }6 }7}
Logically the progression: if the item is not null
and if the Set
does not contain the item, then add it.
The problem is not only the perceived complexity of such a simple action, but that the primary action of this code is buried at the deepest level.
Ideally the primary action of a block of code is at the top level. We can refactor the conditional to a guard clause to unravel the nested code and expose the primary action.
A guard clause simply protects our method from exceptional paths. Although they commonly appear at the top of a code block, they can appear anywhere. We can convert any nested conditional into a guard clause by applying De Morgan's Laws and relinquishing control. In code, this means we negate the conditional and introduce a return
statement.
By applying this to the add
method our implementation becomes:
1public function add($item) {2 if ($item === null || $this->contains($item)) {3 return;4 }5 6 $this->items[] = $item;7}
In doing so we have not only drawn out the primary action, but emphasized the exceptional paths for our method. It is now less complex for future readers to follow. It's also easier to test as the exceptional paths are clearly drawn out.
Switching to if
A switch
statement is a very verbose code structure. switch
inherently has 4 keywords and 3 levels. It's a lot to read even if the contained blocks of code are only a few lines. While this is acceptable in certain cases, it's not in others.
There are a few cases where using if
statements instead of a switch
statement may produce more readable code.
- When there are just a few
case
blocks the inherent structure of theswitch
statement produces more lines than the equivalentif
statements. - When
case
blocks contain nested code the complexity increases and readability decreases to a critical level. Using guard clauses or adopting the practices within Big Blocks can improve the code. - When type casts or calculations are needed to coax the value into the constraints of a
switch
. This doesn't apply to languages (Swift, Go, etc) that support more complexcase
comparisons.
switch
statements are best when a 1:1 ratio exists between case
statements and the lines of code within their blocks. Whether these lines are assignments, return
statements, or method invocations the readability remains nearly the same provided the ratio is roughly 1:1.
1switch ($command) { 2 case 'action': 3 startRecording(); 4 break; 5 case 'cut': 6 stopRecording(); 7 break; 8 case 'lights': 9 adjustLighting();10 break;11}
Note: In such cases where switch
statements are streamlined, many programmers use a map, database table, or polymorphism instead. All of which are indeed additional alternatives. Just remember every solution has trade-offs (complexity). A switch
statement is often "good enough" for most code.
Complex loops
Another common form of nested code are loops. Loops by nature are complex. As programmers, we're cursed to be off by one and miss incremental logic. Again, we're humans not computers. So we're unlikely to win the battle against loops. They will always challenge us. The only way to combat this complexity is readability.
I won't get into which data structures and algorithms may help improve your code. That is much too specialized. In general though, most loops deal with accumulation or invocation. If you find your codebase contains lots of loops, see if higher order functions like filter
/map
/reduce
can be used. While this may not improve readability for all readers, it will improve your individual skillset.
Want more tips? This practice is taken from BaseCode - a field guide containing 10 real-world practices to help you improve the code you write every day.
Find this interesting? Let's continue the conversation on Twitter.