A guard clause is a snippet of code at the top of a function or method that returns early when some precondition is met. I believe that we can reduce both the complexity and cognitive load of code by using them appropriately.
We’ll first explore several versions of a sample method from a hypothetical billing system. For these purposes, we’ll assume this is code in an existing system and we’ll look at refactoring it to reduce complexity and make it easier for a programmer to understand. The first example will be trivial enough to easily understand, but we’ll build on it in the final examples.
Code with Traditional If/ElseFig 1. Code with a traditional if/else
calculateSalePrice will return the sale price of an item. In this method the implementation follows the ‘one return only’ rule. The first line offers a default value that would be disastrous if it were returned, and we’ve got some nesting that will take a moment to mentally parse and fully understand.
Code with If/Else and Early ReturnFig 2. Code with If/Else and Early Return
We’ve certainly reduced some of the boilerplate, and we’re sneaking up on a smaller implementation. We’re making progress, but essentially all of our code is nested one level deep.
We’ve reduced some cognitive load by removing the
$price = 0; default price. Because returning a zero for price would be bad, a developer may have spent a little time looking to confirm that a zero price would not be returned. If multiple developers looked at this many times over the life of a project, the time spent starts to add up.
Code Without ElseFigure 3. Code without and else clause
The logical flow of this is identical to Figure 1 and 2. However, there is now an explicit return statement, and we’ve reduced the function by two more lines of code.
While we have greatly reduced the amount of boilerplate from Figure 1, we’ve ended up with a function where the primary goal of the function is nested within an
if. In refactoring with guard clauses, we want to be able to skip reading the guards and quickly see the primary goal of the method. In this case, the primary goal of the function is to run the
getPrice() method. We’ll do some further refactoring to make sure that we are very clear in our intent.
Code Without Else with Inverted ControlFigure 4. Code without an else and a proper guard clause
The logical flow of this is the opposite of Figure 3. We’re now using a proper guard clause. We have moved the primary goal of the function into the main body without any nesting, and we’ve handled the less common case with a guard.
By moving our primary goal of the method to the main body of the function, we’ve greatly simplified the method. We have reduced the cyclomatic complexity and have made it very clear under what conditions we don’t perform the tasks of our main goal.
In addition, in this case I really like the Symfony style rule to place a blank line above a return statement. This makes it very clear what the main purpose of this method is.
At this point in the refactoring, you may notice that this code has been a ternary all along. You may jump to the conclusion that you should simply rewrite this as a ternary and move on. However, if during maintenance you needed to add a second guard clause, you would have a diff of 100% for the method when you introduced that change. We’d be saving a few bytes of code now for a harder pull request review later.
A More Practical Code Example
This next example is more believable and has a higher initial complexity than our previous example. With a little imagination you can see how it could have grown into its current state.
As an exercise, please read the code below. In simple terms, under what conditions can the post be edited?
In this code, a post is editable if:
- The user is the editor of this post or an administrator
- The user is the author and the post was written in the last thirty days.
The next time that we have to work on this code, we’re going to have to do a lot of thinking. This small puzzle has a check for an editor role with a value of
3 and an administrator with a value of
5 and there are timestamps involved. We may even need to draw a truth table on the back of a napkin or a whiteboard to fully understand a statement like this.
Imagine being given a new requirement: “A supervisor of role
4 can edit posts used in the sidebar.” The single
if statement has gravity; it pulls us in. We’re likely to see the mess, surrender a little bit, paste in an extra
|| ($user->getRole() == 4 && $this->getCreatedOn < time() — 518400), and then move on to the next project. A complicated statement like this has a tendency to grow over time.
Let’s look at refactoring this to make any future maintenance easier. We can also think about how we could have written this from the very beginning in such a way that it could grow over time in a more manageable way.
Initial Refactored CodeFigure 6. Refactored with multiple conditionals
Imaging scanning this code. We scan from bottom to top and immediately see that the default return is false. We can think to ourselves “Ok, we’re safe. Things aren’t usually editable.” Then we’re free to look through the method to see the conditions where the post is editable.
You’ll notice that lines 12 and 16 could be combined into a single
or statement. If we decided to do that, we would increase complexity and decrease readability. I don’t think we’d save anything other than line count.
Let’s revisit the new requirement of “A supervisor of role
4 can edit posts used in the sidebar.” With this newly refactored code, this should be a much simpler task. We’ll add a guard for this new requirement and then look at the diff.
Initial Refactor with a New RequirementFigure 7. We’ve added special handling for a sidebar post
Git DiffFigure 8. An easy to read diff
This is a very easy diff to review. It adds no complexity to any existing code and it is entirely self contained. We could review a diff like this in a pull request and have confidence that no other comparisons have been incorrectly updated. In addition, by using intermediate variables, we’ve documented what a sidebar post is without having to leave a comment.
Refactoring for Growth by Removing Magic Numbers
Imagine that we need to change the type that identifies our sidebar posts. The sidebar posts are currently type
8. We’ve probably got that number spread throughout the code in quite a few places. If we need to change that value to
11 our only option is to to string search through the code and hope that we find all instances of the number
8 and correctly identify them all. And we can only hope that it’s not expressed as
$type = $primaryType + 1; somewhere!
We could save ourselves from this ordeal by creating a constant in the
Post class to represent the sidebar type. We could add a new
const SIDEBAR_TYPE = 8; and then use this throughout our code. We could then convert our comparisons to look like
$isSidebarPost = $this->getType() == self::SIDEBAR_TYPE;.
Removing magic numbers is always a positive change. If new requirements dictated a change in sidebar type, we can make that change in a single place. The diff would once again be very small. By removing those magic numbers, we’ve added some future-proofing and we’ve added some self-documentation. However, we can still do better.
Let’s imagine another growth scenario where we’re going to add a second sidebar type. We now need both
11 to be a sidebar post. Our simple constant idea breaks down. Our task would be to find all of the locations in the code that use
self::SIDEBAR_TYPE and update them to compare to two different constants. Instead of distributing this logic throughout our code, we’ll refactor and simplify.
We’ll add a new method that we can use in place of these comparisons. It will look like
$isSidebarPost = $this->isSidebarPost();. This new method consolidates this checks to a nicely encapsulates the logic to a single reusable method and gives a simple location for all future refactoring. We’ll include this our final example.
A more complete exampleFigure 9. Refactored and ready for a long service life
There is certainly still room for debate. We’ve created some extra variables and we’ve added some extra lines. However, we’ve greatly simplified the code and we’ve made future maintenance less error prone and easier to do.
- Reduce complexity whenever possible by introducing guard clauses
- Make the main purpose of the method the most obvious execution path
- Think about writing for growth to minimize future complexity
Go forth and make great things!