When I looked up the publishing date for this book, I was surprised to find out that the book is 11 years old. The content feels as if written today. The authors’ advice is as relevant as ever. I am sure that it will stay relevant at least for the next 10 years.
The book is organised in 15 chapters and 4 parts. Each chapter comes with many instructive code examples. I read each chapter in 30-45 minutes. I read the chapters out of order without losing track. For my review, I picked one chapter from Part 1: Surface-Level Improvements, one from Part 2: Simplifying Loops and Logic and one from Part 3: Reorganizing Your Code.
Chapter 10 is my favourite as it is the essence of making your code more readable. The key idea is to write code on a higher abstraction level by delegating lower-level code into library functions and classes with telling names.
Chapter 5: Knowing What to Comment
The key idea for what to comment and what not is this (emphasis mine):
“The purpose of commenting is to help the reader know as much as the writer did.”
p. 46
What NOT to Comment
You need not comment a constructor with
// Constructor
Account();
or a getter with
// Returns the balance of the account
qreal getBalance() const;
These comments don’t provide any additional value over the code.
If bad names are compensated with detailed comments, it may be a good idea to turn the comments into good function names. The golden rule is that good code is always more readable than bad code plus comments.
Recording Your Thoughts
Include “Director Commentary”
You could tell the reader that you tried other solutions and why you didn’t choose them. This prevents readers from wasting their time on trying out these other solutions.
If, for example, you define a singleton for QML that you also want to use in C++, it looks like a memory leak. You better write a comment that it isn’t and that the QML engine will take ownership. What looks like a bug need not be one. And you better say so in a comment.
Comment the Flaws in Your Code
Mark flaws in your code with tags like TODO, FIXME, HACK or XXX (danger). These markers help you and your co-developers to keep track of all the pending micro-tasks.
When I try to make a failing micro-test pass again, I often see scenarios for the next micro-tests. I quickly write a TODO comment, carry on with my current micro-test and come back to the TODO later. Before I push my changes, I check whether I have fixed all the TODOs.
Put Yourself in the Reader’s Shoes
Anticipating Likely Questions
You can ask yourself two questions about your code: “What is surprising about this code? How might it be misused [or misunderstood]?” Then write down the answers as comments.
Recently, I had to check when a hardware button is pressed and released. The library function blocked until an event occurred or until a timeout (1s) happened. The blocking function call is repeated forever. If this function was called in the main GUI thread, the GUI would regularly freeze for one second. I wrote a comment why the loop with the blocking function must run in its own thread – anticipating questions about the use of an extra thread.
“Big Picture” Comments
Big picture comments describe “how classes interact, how data flows through the whole system, and where the entry points are”. You could use message sequence charts to visualise the interaction between classes in several layers. I often scribble down call sequences in my paper notebooks. Just adding photos of these sequences to the documentation will help you and others.
You may know CRC cards for designing object-oriented software. CRC stands for Class-Responsibilities-Collaborators. You can use CRC as the blueprint for your comments. Big-picture comments list the Collaborators and why the Class interacts with these Collaborators.
Chapter 7: Making Control Flow Easy to Read
The key idea of this chapter is to
[… write] all your conditionals, loops, and other changes to control flows […] in a way that doesn’t make the reader stop and reread your code.
p. 70
This is good advice in general: Write all your code so that readers understand it on their first read.
The Order of if/else Blocks
The authors give three rules of thumb for the order of if/else cases.
- “[Deal] with the positive case instead of the negative.”
- “[Deal] with the simpler case first to get it out of the way.”
- “[Deal] with the more interesting or conspicuous case first.”
Which rule applies is mostly clear, but is sometimes a judgement call.
Avoid do/while Loops
I don’t even know when I last used a do/while loop. But I never thought about the reason. The authors did. You must skip to the end of the loop to see when it is repeated. All other control-flow statements put the “logical conditions […] above the code they guard”.
Returning Early from a Function
Returning early from functions reduces the nesting of code significantly and makes the code easier to read. In contrast, enforcing the one-return-per-function policy increases nesting and unreadability. The authors are very clear about this policy: “This is nonsense.”
The authors dedicated a complete section to Minimize Nesting. Returning early from functions is a powerful method to achieve this goal. In loops, you can use continue as an equivalent to returning early.
The Infamous goto
Goto is not always evil. In C code, it can be put to good use. Instead of writing the cleanup code after every failed if-condition again and again, you can write a goto exit. You write the cleanup code once at the exit label. In C++, destructors take over the cleanup. Hence, there is rarely a need for goto.
The ?: Conditional Expression (a.k.a. “Ternary Operator”)
The authors’ advice is to use the ternary operator only for very simple cases. You shouldn’t nest ternary operators. You definitely shouldn’t use them to “squeeze everything on one line”.
This leads the authors to some timeless and general advice (emphasis mine):
Instead of minimizing the number of lines, a better metric is to minimize the time needed for someone to understand it.
p. 73
Chapter 10: Extracting Unrelated Subproblems
The advice for this chapter is to aggressively identify and extract unrelated subproblems. Here’s what we mean:
1. Look at a given function […] and ask yourself: “What is the high-level goal of this code?”
2. For each line of code, ask: “Is it working directly to that goal? Or is it solving an unrelated subproblem […]?”
3. If enough lines are solving an unrelated subproblem, extract that code into a separate function.
p. 110
This is an excellent description of the Single Responsibility Principle (SRP). And it is more: It gives you a practical procedure how to ensure that a function has only one responsibility. The unrelated lines of code are additional responsibilities of the function.
Introductory Example: findClosestLocation()
The example function findClosestLocation()
goes through a list of locations and finds the location that is closest to a given location on a sphere. For each location in the list, the function calculates the spherical distance to the given location and updates the closest location if necessary. The goal of the function is to determine the closest location. The unrelated subproblem is to calculate the spherical distance.
Once the calculation of the spherical distance is extracted, the function is much shorter and more readable. The reader doesn’t need to understand the geometry for calculating the spherical distance to understand the function findClosestLocation()
. All lines of this function are now on the same abstraction level. The lines for calculating the spherical distance were on a lower abstraction level and are now hidden in the function sphericalDistance()
.
Pure Utility Code
The function sphericalDistance()
is a good example of pure utility code or general purpose code. Such functions are used in many other applications. So, you should put them in a library for easy reuse.
It’s fairly easy to spot pure utility code:
In general, if you find yourself thinking, “I wish our library had an
p. 112XYZ()
function,” go ahead and write it! […] Over time, you’ll build up a nice collection of utility code that can be used across projects.
I think that the Qt developers are fairies who have granted our wishes long before we knew about them. The Qt libraries are full of pure utility functions and classes. By the way, QGeoCoordinate::distanceTo(const QGeoCoordinate&)
is the Qt function for calculating the spherical distance.
Create a Lot of General-Purpose Code
The authors urge you to create your own libraries with general-purpose code (often called utils) or to use third-party libraries. You can reap the following benefits.
General-purpose code is great because it’s completely decoupled from the rest of your project. Code like this is easier to develop, easier to test, and easier to understand. If only all of your code could be like this!
p. 114