Outerhout's "A Philosophy of Software Design"
Andreas Hohmann February 27, 2024 #software #design #book reviewI keep my expectations low when buying books on software design. It's hard to write about software without referring to a concreate project or narrower topic. More often than not, any general advice has to be watered down by the various tradeoffs that are unavoidable in real projects.
I was therefore pleasantly surprised when I picked up the second edition of John Ousterhout's A Philosophy of Software Design and found myself getting more and more excited as I went through the chapters.
Software development is all about managing complexity (no disagreement here). The author's definition of software complexity as "anything related to the structure of a software system that makes it hard to understand and modify the system" may not be the most comprehensive or measurable, but sufficient for guiding software design.
Outerhout identifies dependencies and obscurity as the top two causes of complexity. Again, my guess is that most practitioners will agree. We also agree that complexity typically accumulates in small steps over time. Some special case here, some dependency there. How can we prevent this or at least slow down the decay? Outerhout argues for strategic programming that takes a long-term view in contrast to the tactical programming that tries to implement the next feature as fast as possible (only to slow to a crawl as time passes). The rest of the book explains what strategic programming entails.
The common thread throughout the book is the notion of "deep" modules, that is, functions or classes with small interfaces and rich implementations. To tame software's complexity, every module should shield us from some complexity and expose a clear, easy-to-use interface while hiding the intricacies of how the functionality is implemented. A shallow module whose interface is not much smaller than its implementation does not provide much value. Similar interfaces in different layers and "pass-through variables" are red flags.
Ousterhout is not a fan of the decorator pattern, for example, which uses thin classes adding a small piece of functionality while delegating most functionality to the wrapped object. He also dislikes the thin layers of Java's IO stream design (distinguishing streams from buffered streams) and mentions this design as a negative example in multiple places throughout the book.
I agree with the assessment of decorators for the most part. A common counter example is an adapter class that is mainly avoiding dependencies rather than providing new functionality. There are also examples of good decorator-based GUI APIs (such as flutter's widgets). I suspect that they work because of the small interface.
I would also add that the "depth" of a piece of code is not a direct function of its length. If your environment provides powerful abstractions (functional languages such as Clojure come to mind), a one-line function can be deep.
Most chapters tackle the question of how to organize software to minimize complexity. Outerhout mentions a few points that are often missed.
First, there are many ways to avoid complexity. Does a parameter really need to be exposed in the API or can it be set by the software itself? Can an error condition be avoided or handled by the implementation rather than passing it on to the caller? He points to the handling of file deletion across processes in Linux as a good solution avoiding error cases (that I wasn't even aware of).
Second, it's worth investing time (the strategic programming view) on "general-purpose modules" which are deeper. In my experience, more general and abstract solutions turn out to be simpler and often even faster than the specialized ad-hoc solutions resulting from tactical programming.
Finally, I couldn't agree more with Outerhout's view of documentation. Just like unit testing, I consider documention one of the best means to assess your design early on. If you cannot describe it concisely (or test is easily), there is probably a better design waiting to be discovered.
I'm shocked every time I find source code that is completely void of documentation. If a class doesn't have a summary comment, what's its purpose? Is it needed? Should it be deleted? Uncle Bob has done significant damage to the software industry by calling in-code documentation a failure. I'm the first one to insist on clear code, but I've also found that any non-trivial code deserves a good comment.
There are a lot more gems in this book than I can mention here, and I highly recommend reading it cover to cover.
However, there is one thing I don't agree with: Never use num
or numOf
as a
prefix for a "number of" variable such as numThreads
. A count
suffix such
as threadCount
is clearly the only sensible choice 😉. In general, suffixes
lead to better looking code, see TigerStyle's
naming.