## Introduction
I picked up this book after reading an interesting blog post that mentioned it in passing. Given Lars’ recommendation I knew the book would be good.
I usually don’t have much time for reading and this book sat on my desk for a week or so before I caught COVID again and suddenly found myself with time to kill while otherwise lying on the couch feeling very average.
For context, I’ve been writing software professionally (and as a hobby) since 2011 in a variety of compiled and scripting languages including JavaScript, Ruby, C, Python, and Go. So for about 13 years. I’ve most recently been working in Go and I’m familiar with the idioms of that language, which certainly shaped my response to the book.
The first thing I noticed about the book is its refreshingly small physical size. At only 178 pages it is easily digestible over a couple of days of on-and-off reading.
The central thesis of the book is that good software design is about minimising the complexity of a software system. The author provides a definition of complexity, give some tips on how to recognize it, and then goes on to describe common scenarios you may be faced with while writing software, and how to approach them without introducing unnecessary complexity.
The author’s philosophy aligns almost entirely with opinions I’ve formed over the years. However my opinions are few, unstructured, and little more than an intuition about what makes “good code”. So what I really liked about the book is how it takes such ideas and shapes them into discrete concepts, each with a concrete description and examples.
I also appreciated the way that the author compared and contrasted his advice several times to two other bodies of work on a similar topic: the famous Clean Code by Robert C. Martin, and the Go programming language’s Effective Go.
## Tactical versus Strategic development
One of the first issues the author raises as a cause of complexity is tactical versus strategic programming (Chapter 3). Tactical programming is described as a “short-term mindset” which is:
.. focused on getting features working as quickly as possible.
On the other hand, strategic programming is described as keeping “great design” of the overall system as a priority, where you:
.. invest time to produce clean designs and fix problems.
I love the characterisation of these two approaches to software development.
“Working code” is a necessary but not sufficient attribute for good software design, but it is easy to lose sight of this when you are deep in the weeds of a feature or bug fix. I personally found this idea difficult to accept early in my career: after all, what could be more important than working code? However having experienced the long-term effects of tactical programming on a codebase, I can agree that incremental investment in strategic programming over time does tend to reduce the complexity of the codebase over the long term.
I especially liked this description of the “tactical tornado”:
Almost every software development organization has at least one developer who takes tactical programming to the extreme: a tactical tornado. The tactical tornado is a prolific programmer who pumps out code far faster than others but works in a totally tactical fashion. When it comes to implementing a quick feature, nobody gets it done faster than the tactical tornado. In some organizations, management treats tactical tornadoes as heroes. However, tactical tornadoes leave behind a wake of destruction. They are rarely considered heroes by the engineers who must work with their code in the future. Typically, other engineers must clean up the messes left behind by the tactical tornado, which makes it appear that those engineers (who are the real heroes) are making slower progress than the tactical tornado.
Is there any software developer who hasn’t worked with someone like this (even if it was their younger selves)?
## Pass-through variables
In §7.5, in the context of API duplication across layers, the author discusses the problem of pass-through variables which is described as:
… a variable that is passed down through a long chain of methods.
The author compares several approaches to fixing this issue, eventually landing on a least-worst approach of creating a global context
object, putting the cert
variable in there, and then “a reference to the context can be saved in most of the system’s major objects.”
In my opinion this approach has several significant drawbacks:
- The context becomes becomes a grab-bag of unrelated data which may be used widely across the software system;
- There is little difference between such a context and a global variable, so it becomes more difficult to reason about the behaviour of the code; and
- Objects which contain such a context need to know that they should interact with certain fields but not others, and it is not at all obvious from the code which fields of the context can be safely accessed or mutated, and which should not be touched.
Go Code Review Comments even addresses this explicitly:
Don’t add a Context member to a struct type; instead add a ctx parameter to each method on that type that needs to pass it along.
Instead, an approach that I think was missed is to refactor the code so that the function using the variable becomes a method of an object which stores an internal copy of the variable. This way the variable is available whenever the function is called without needing to pass it all the way down the call stack explicitly. Even if the same variable is required elsewhere, I would prefer to copy it into various objects that need it. I find this design much cleaner than storing the variable alongside unrelated data in a common context that may be used in objects which have no need to access the variable.
## Function length
In §9.8, the author discusses how to structure functions; in particular when to break them up into smaller functions. I agree with their conclusion that you should optimise for reducing overall complexity rather than, as Clean Code proposes, prefer to optimise for reducing function size.
However one consideration that the author doesn’t mention is that the longer a function is, the more difficult it becomes to read and understand purely due to the mental context that must be held while reading. Smaller functions just have much less state to hold in your head in order to understand them, and I think this is a strong argument for breaking up large functions. The idea that you should reduce the amount of mental context for readers is similar to the concepts of indenting error flow (from Go Code Review Comments), and reducing nesting (from the Google Testing Blog).
## Comment first
In §15.2 the author proposes a “comment first” approach to software development and I could not agree more. If a function is small and obvious, then a simple interface comment will do. But if the function is more than a few lines of code then my favourite approach is to start writing comments in the body of the function first. I find this has at least two major benefits:
- It helps me mentally break down the task. The comments become something like a step-by-step recipe for how the task will be completed. And I often find it takes a few iterations of refactoring the comments for the abstractions and design of the function to emerge. This is much easier to do early in comment form than it is later once the code is written.
- As you write the code beneath each comment, it becomes the equivalent of checking off a TODO list, and is very satisfying.
Even if code evolution causes early comments become inaccurate I think commenting first is still a good idea:
- Inaccurate comments help to draw attention to potential bugs in code review. After all, if the implementation doesn’t match the comment, it could simply be a bug in the code.
- It is easier to correct an existing comment than it is to add a new comment.
- An incorrect comment is likely to be fixed by leveraging “Cunningham’s Law”:
The best way to get the right answer on the Internet is not to ask a question; it’s to post the wrong answer.
## Unit tests
On unit tests (§19.3), the author says:
With a good set of tests, developers can be more confident when refactoring because the test suite will find most bugs that are introduced. This encourages developers to make structural improvements to a system, which results in a better design. Unit tests are particularly valuable: they provide a higher degree of code coverage than system tests, so they are more likely to uncover any bugs.
This is such a great paragraph! Fearless refactoring is a super-power that has to be experienced to be fully understood. And such fearlessness is only achieved through healthy unit test coverage.
## Test-driven development
The author declares themselves “not a fan” of TDD (emphasis theirs):
The problem with test-driven development is that it focuses attention on getting specific features working, rather than finding the best design. This is tactical programming pure and simple, with all of its disadvantages.
They go on to describe a narrow scope for applying TDD:
One place where it makes sense to write the tests first is when fixing bugs. Before fixing a bug, write a unit test that fails because of the bug. Then fix the bug and make sure that the unit test now passes.
I agree completely with this summary of TDD. I have tried applying it to development work and fallen into the trap of just getting things working rather than considering if the design is correct. And I’ve also seen bugs caught early by unit tests written when fixing the same bug previously.
## Conclusion
A Philosophy of Software Design is well worth reading in its entirety for anyone who writes code. John Ousterhout had done a masterful job of distilling his opinions and experience into a coherent structure, and has done so in a wonderfully concise and readable way. Some of his advice may seem overwrought to inexperienced developers; similar advice certainly did early in my career. But I think revisiting his ideas from time to time will be invaluable to inform the development of your own software design philosophy.
And I’m sure experienced developers will nod along with almost everything Ousterhout has to say.