Several software projects I worked on recently came to an end or a milestone, so I have been reflecting on how those projects could have been made better. Although I’m proud of these projects, I noticed that these projects were actually harder to program towards the end than the beginning. So, I have been studying how to create code that is easier to read, maintain, and scale:
I’ve studied the SOLID principles, which I wrote about in this blog post.
I also read Clean Code by Robert C. Martin, one of the seminal books on code organization. I learned a lot from this book, so I thought I would share six eye-opening ideas and metaphors from it:
1. Your code should be organized like a newspaper.
Most newspapers go from the general to the specific - a headline will tell you the big picture, then a series of subheadings and paragraphs will fill you in on the minutia. You code should mirror this structure: it should start abstract then get progressively more specific.
Code written this way might look like this:
Why this structure?
Firstly, code written this way is more readable. Instead of jumping between high-level and low-level details, the reader is eased into the specifics of the code.
More importantly, this structure avoids duplication: my IncreaseBranches code is only in one place, so if something is wrong with the number of Branches in a Tree, I know exactly where to look.
2. Code should be “shy.”
Throughout the book, Martin personifies his code in instructive ways. One way he does this is by saying that code should be, “shy.” What he means by this is that a class should only talk with its immediate collaborators.
Here is an example of overly extroverted code from a tower defense game:
This code has many issues, but the biggest is that LoseArea is using UIManager to talk to PlayerBaseHealth. The problem with this setup is that if UIManager needs to change, the link between LoseArea and myPlayerBaseHealth will break, causing errors. Using UIManager as an intermediary also makes this code more complex and harder to debug.
Shy code only talks to the classes it needs to, so we cut out the middleman and avoid issues like this.
3. Eliminate functions with Boolean parameters if they contain two different sets of functionality.
When writing code, we want our classes and functions to do one thing. (I go more into this principle in the Single-Responsibility section of this post.) When we create a function with a bool parameter, we invite the possibility of creating a function that does two things.
Let’s say we have a Soldier class that we need to make either Ice or Fire Damage resistant.
We could write the code poorly like this:
There’s a lot wrong with this function, but the biggest issue is that this function is two functions in one. This is problematic because it adds ambiguity to our code. If we’re not familiar with this function, we are forced to ask ourselves whether “TRUE” maps to gaining Fire OR Ice resistance.
Additionally, Fire or Ice Resistance is a false dichotomy. Let’s say our producer wants us to add poison damage to the game. If we want to use this function to give the soldier Poison resistance, we will need to rewrite it.
It’s better to split out the functionality into more specific functions:
The functions’ responsibilities are now clear from their names.
4. Code should not be “envious”.
An envious class heavily manipulates the variables and functions of another class.
In the example below, our Farmer is too involved in the inner machinations of Tree – Farmer is envious of Tree’s functions and variables.
If a class is manipulating variables that don’t concern its primary functionality, it’s probably envious.
Why don’t we want envious classes?
As with many of the principles above, envious classes make our code both harder to debug and more error prone. Let’s say our Trees contain less Fruits than we expect. Our first instinct might be to search Tree – but the functionality that makes the fruits null (which is problematic for other reasons) is not there. It’s in Farmer.
Putting HarvestFruitTree in Farmer also invites duplication or coupling. If a lazy programmer wants another class to Harvest Fruit, they might have a class:
· Make its own implementation of HarvestFruitTree, creating duplicate code.
· Call HarvestFruitTree through Farmer, coupling the classes together.
We would fix the example above by moving the HarvestFruitTree function into the Tree class or one of its subclasses.
5. Your code should be a staircase of increasingly specific functions.
As you go from high-level to low-level functionality in your code, you should encapsulate each step in a well-named function.
In the FruitTree class below, we get a detailed description of how the tree grows by following the calls made by GrowOverall():
GrowOverall()
GrowRoots()
GrowBranches()
IncreaseNumberOfBranches()
GrowFruitsOnBranches()
GrowTrunk()
Viewed this way, you can see that our code steps down from the abstract to the specific.
This pattern of organization is ideal because it makes our code more modular. If we wanted our tree to grow only its roots for some reason, we could call GrowRoots instead of searching GrowOverall for this functionality.
6. The principle of least surprise.
Martin notes that impressive code is not necessarily code that completes some opaque, complex task but rather code that acts exactly as you would expect it to. In introducing this principle, Martin quotes Benjamin Franklin:
“A place for everything and everything in its place.”
Here is a screenshot of a Warrior class that does NOT adhere to this principle. You’ll see in my comments where it could be improved.
Final Notes:
Clean Code was an amazing book and it made me a better programmer. I would recommend it to anyone who’s interested in making their code easier to maintain and scale. It is quite Java heavy, so you will get most out of the book if you know Java.