Guard Clauses: Three Guidelines to Ensure Code Simplicity for Software Developers
Software developers perform a multitude of tasks. We gather and analyze requirements, design software, challenge stakeholder decisions, and finally, after all, we create and maintain software.
In this article I’ll refer to a few “facts”, which might not really be facts, but for the sake of argument, let’s pretend they are. These “facts” were listed some years ago in a very relevant article titled “Frequently Forgotten Fundamental Facts about Software Engineering”, written by Robert L. Glass and published in the IEEE SOFTWARE magazine, May/June 2001 edition. The facts outlined in this piece can help us better understand the logic and philosophy I’ll develop in this article.
The first thing I want to bring to your attention is Glass’ take on software quality. Glass explains that quality is a collection of attributes, and lists a few of them on which there’s a general consensus: portability, reliability, efficiency, human engineering, testability, understandability, and modifiability. Understandability and modifiability are the two key terms we’ll be focusing on. In my opinion, these are the factors that are more closely related to humans.
Another interesting fact the article brings up is related to what I’ve mentioned above: how software developers spend their time creating and maintaining code. Glass explains that maintenance typically consumes an average of 60% of software costs. Therefore, it is probably the most important life cycle phase.
This means maintenance is the costliest phase, and that quality is related to understandability and modifiability. If you think about it, you create the software only once, and then it goes to production where stakeholders use it in real life. These stakeholders then start noticing things that slow them down, that frustrate them, that could be improved, or that simply don’t work as expected. They then do what we’d do: ask the software maintainers to add features, or fix bugs. In other words, they ask the developers to maintain the software.
Let’s think about it as any other product that requires maintenance. Think about your car if you have one, or the faucet of your shower. These two items were manufactured only once, and will require some maintenance across their lifetime. However, such maintenance will not add any additional features to them, they will only maintain them in good operative conditions.
The case of software is different. Think about your computer or cellphone: they were manufactured only once, exactly like the car and the faucet, but their operating systems are being routinely updated to add new features such as better security, more stability, better performance, etc. Unlike the car and faucet, in this case the hardware stays intact but the software changes.
This is arguably the best quality about software: it is easy to change. It is soft, unlike hardware. A car is hardware. A faucet is hardware. Simply put, these things are hard to change.
In theory this sounds relatively simple. But in practice it might not be as straightforward. Whether a software developer or not, my question to you is: How soft is your software? In other words: is your software easy to change? Assuming that the best part of software is that it’s ‘soft’ and easy to change, if you design your codebase in a way that is not easy to change, you are arguably nullifying software’s biggest differentiating advantage.
Another interesting fact outlined in Glass’ article is as follows: Most software development and maintenance tasks are the same, except for the additional maintenance task of “understanding the existing product”. This task is the dominant maintenance activity, consuming roughly 30% of maintenance time. So, you could claim that maintenance is more difficult than development. It’s no surprise that software developers usually find creating something new much more enjoyable than fixing old bugs, but “maintenance” is not only about bugs. As per Glass, ‘enhancement’ accounts for roughly 60% of software maintenance costs, whereas error correction accounts for roughly 17%. He concludes that software maintenance is largely about adding new capability to old software, not about fixing it.
Regardless of the semantic accuracy of Glass’ stats, it’s safe to say that we spend much more time maintaining software than we’d like to. Another sad reality is that, unless your software is decommissioned, you’ll need to keep maintaining it, gradually increasing the amount of time per day you’ll spend maintaining software.
While maintenance itself may be inevitable, there is one aspect of this which we can control: minimizing the time required for maintenance of the code we produce. In this article, I will explain how.
In order to write code that is simpler and faster to maintain, I have put in practice three guidelines which I follow both for my own code, and when reviewing pull-requests for my team. I have formulated these methods over the years and while I hope you will find value in them, please note that you know your circumstances best, and someone else’s solutions may not guarantee success for you.
We’ll start with a bad example. The following is a snippet of old, legacy code, but a real case nonetheless. While the code works, I’m not so sure if I would have approved it in its current state. Let’s have a look:
I’m sure you’ll agree that this code isn’t easy to maintain. We’re going to improve it following three simple guidelines. It would be impossible to dissociate them and produce code samples for each individually. Instead, let’s list them sequentially, explain them, and finally apply them together, all at once. I promise there’s no rocket science involved.
Guideline #1: if there’s a good reason not to do something, don’t do it.
In this section, I am not advising you against being helpful, trying new things, or going beyond your role. Rather, I am suggesting that for every thing you do as a developer, challenge yourself to come up with a good reason to not do that very thing, and if such a reason exists, I suggest not doing it.
Let’s visit a concrete example: someone asks you to backup a database in production. They have good reasons to ask you to do so: they need a backup, nobody else can do it, the database server might crash, and many other good reasons that all sound very convincing. Would you do it?
Depending on the context, I can think of a few good reasons not to: having a backup done by someone that is not an expert on the matter might create a false sense of security. For example, do you know if your user has read permissions over the entire dataset that needs to be backed up? Could you possibly miss something in your backup? Does this server contain any information that, for compliance reasons, should be stored with certain security standards? In the case of needing to restore the database, can you guarantee that it will be possible to use your backup? “Unknowns” can often be a good reason not to do something.
Guideline #2: if you are not going to do something because you have a good reason not to, make sure this reason can be clearly explained.
When you refuse to do something, someone typically gets frustrated. And more often than not, the person that asked you to do a backup did not ask because they thought you looked bored.
Let’s work on this together to clearly explain the reasons for your refusal. In the example given, it’s true that the problem of a production database without a backup exists, so the solution might be to find the correct resource to handle this task, or to find someone else that could first clear some of the unknowns.
The second guideline thus aims to help the requester understand that it’s in their best interest to find the person that has the requisite knowledge to handle the given task. You’re not refusing to do something, you’re avoiding creating a situation that will spawn new problems in the future. Let’s say that the DBA on-call receives an alert at 2AM with any one of the following issues: they try to find the backup and the format is not right, it’s not placed in the correct folder, the naming convention does not match, you zipped the backup instead of gzipping it, you stored it in a server that removes files automatically every Sunday at exactly 12PM, etc…
To avoid such frustrations, clearly articulating reasons for not performing a task can go a long way. “Unknowns” can also be understood as not knowing how to handle a certain problem.
Guideline #3: keep in mind ‘la raison d’être’ of a function. The primary function should reside and be evident at its root level.
‘La raison d’être’ — which directly translates to the reason for existence, or reason to be — is the most important reason or purpose for someone or something’s existence. The function in the code I used in the earlier example is named “unlock” because the reason for this function’s existence is to release a mutually-exclusive lock, or mutex. In the code above, the actual “unlock” happens in line 19, when the
hDel function is called on a Redis key. It might take you some time to find it, because the ‘raison d’être’ of this function is not formulated at the root level. It is, instead, 3 levels deeper. There are 3
if statements before the code actually releases the lock.
Enough theory, let’s apply the guidelines.
Code is not linear, it has branches, bifurcations, loops, remote calls, and the list goes on… Do you remember those “choose your own adventure” books? They weren’t books that you’d read linearly and sequentially. These were books where you, the reader, could actually curate your adventure and make decisions. So to say: if you prefer to visit the village, read page 76; if you want to join the hiking group, go back to page 10.
Well, imagine the developer reading the story of this software, because the first thing we do is to try to understand the code. Well, the story of this function starts with an ‘if’ condition that performs some validation about the order ID and the type of lock. If the condition is fulfilled, the inner block runs. If the condition is not fulfilled we end up on line 32, where an exception is raised:
Missing/Invalid parameters. Can’t unlock for type: $type with id $orderId.
The exception’s message articulates the reason why the lock can’t be released, which is that the parameters are not valid. This covers the first two guidelines: refuse an action if there is a good reason not to perform it, and communicate the justification clearly. The problem now is that it is not easy to read. Following the analogy of the ‘choose your own adventure’ books, we’re moving too many pages away: the place where the verification is done (line 13) is too many lines away from the explanation of the rejection (line 32). We needed to skip 19 lines between the verification (guideline #1) and the explanation (guideline #2), in a function that is roughly 20 lines long. A computer will have no problem reading this code, however you and I are humans.
Applying the three guidelines will make this code more human-readable. Some developers call these guard clauses, others call it early returns. The first iteration looks like this:
The first ‘if’ and its corresponding ‘else’ were grouped together. Now they are right next to each other, in a sequential, human-readable manner. And we have already followed our first two guidelines.
In this particular case, the function refused to perform an action because of missing parameters. The function wouldn’t know how to handle a lock if these conditions weren’t met. Placing the decision point and the explanation of such decision closer to one another results in software that is much easier for humans to read. Now let’s apply the same logic onto the next ‘if’ statement:
Instead of having:
Oh, this starts to look nicer! Let ‘s iterate again:
It’s important to notice how the main functionality, or the reason why this function was written, is now at the root level. Philosophically speaking, this makes perfect sense, because one of the definitions of root is: the basic cause, source, or origin of something; which is also related to the reason to be.
Another minor way to improve your code is through the first
if statement. It is composed of a set of
OR operators that will evaluate to true as soon as any one condition is proven to be true. This in fact causes a problem because upon handling the exception we wouldn’t know which of the conditions raised the exception. Think of it as the client programmer asking you this question: upon running this function, would you be able to explain exactly which parameter is missing?
A better approach would have been to decompose the complex ‘if’ statement into a set of smaller conditions, which will return a clearer message, in such a way that you would never need to read the code inside the function. The less time you spend reading code, the more code you’ll be able to write.
So, let’s fix that ‘if’ statement, with these minor adjustments:
Much better! I definitely would have approved this in a pull-request.
In the wild, you’ll find some developers with good reasons for not using guard clauses. These are mostly technical reasons that are valid, but I won’t cover them in this article. If you know some of them, drop them in the comments section.
Wrapping up I’d like to quickly summarize the guidelines I shared with you. They are easy to remember, easy to implement, and in my humble opinion, they are easily applied to make code more maintainable:
- If there’s a good reason not to do something, don’t do it.
- If you are refusing to do something due to a good reason, make sure you can explain it clearly. Keep the reason close to the decision point in your code base.
- Always be mindful of a function’s reason for existence, and ensure that the logic for it is formulated at the root level of the function.
These guidelines can be seen as a way to structure both our code and the underlying thinking which produces the code. Ultimately it allows you to code the way you think, and helps you think within a template that can easily be transposed to programming logic.