A colleague of mine pointed me to a book called “A Philosophy of Software Design” by John Ousterhout. It tackles a very interesting point: students of software engineering (and less so self-educated programmers) often times learn a lot about object orientation, algorithms and all kinds of funky theory, but there is very little time spent on practical software design. The preface of the book also raises the thought that this mystical difference in productivity between the best programmers and an average programmer is less likely just rooted in talent, but rather to high-quality practice than can be taught and learnt. Enough reason to take a peek into the book. Here’s my markups:
Core Hypothesis: It’s all about complexity
The author postulates that complexity is the core thing to focus on. The process of writing software is not limited to anything else than the ability to understand what one created. And as software grows, so grows its complexity - usually up to a point where it becomes hard for the engineers to keep all the relevant information in their mental memory. This is where things start getting slow, where bugs are creeping in, and the whole process goes into a downward spiral of more effort, less progress, more people needed to tackle it, even more complexity being created, and so on.
There are basically two methods to deal with complexity: 1. Avoiding complexity in the first place (e.g. by eliminating special cases) 2. Encapsulating complexity (e.g. dividing it into modules so one only needs to understand an interface, and not the whole implementation)
The book also points out that it’s important to be able to identify (unnecessary) complexity:
“The ability to recognize complexity is a crucial design skill. (..) It’s easier to tell whether a design is simple than it is to create a simple design.”
So, what exactly is complexity and how can we identify unnecessary complexity?
“Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system” (..) Complexity is more apparent to readers than writers. If you write a piece of code and it seems simple to you, but other people think it’s complex, then it’s complex.”
Three Symptoms of Complexity
- Change Amplification: This basically means that a simple change requires touching many different places in the code.
- Cognitive Load: How much does a developer need to know to properly modify a piece of code?
- Unknown unknowns: When it’s not obvious which parts of the code need to modified to achieve a certain outcome
Interesting, the book also concludes that the number of lines of code is hardly related to complexity. It’s generally true that very big systems tend to be complex, but often times solutions that require more lines of code are simpler than very concise notations that need to be deciphered first.
Tactical Programming vs. Strategic programming
Good design is declared to be a mindset question, with the two different mindsets of either tactical programming or strategic programming. The focus of tactical programming is to build something that works. The focus of strategic programming is to produce a great design that happens to work.
The author claims that a 10-20% additional upfront cost into a proper design is the sweet spot in terms of an return on investment. However, there is an interesting caveat to those who are not willing to even invest these 10-20%: “once a code base turns to spaghetti, it is nearly impossible to fix it. You will probably pay high development costs for the lifetime of your product.” – and have a competitive disadvantage quite early on.
Practical guidance on good software Design
The book dives into encapsulation / modularization here: “the best modules are those whose interfaces are much simpler than their implementations”.
So a few hands-on tips are:
- Encapsulate powerful functionality and expose it through simple interfaces (“deep modules”, not “shallow modules”)
- Avoid creating too many classes (especially you Java guys out there) - each of them needs in interface and the sheer number drives cognitive Load
- Try building functionality you need today, but design the interface with multiple use-cases in mindset
- Avoid “pass-through” methods that just call other methods and where the signature is bigger than the actual code
- It’s more important for modules to have simple interfaces than a simple implementation
- Don’t be afraid of big methods if that allows to read them easily (“In general, developers tend to break up methods too much”)
- Design it twice: consider multiple options instead of jumping on your first idea
- Documentation inside the code is essential to properly explain your abstractions (“without comments, you can’t hide complexity”)
- Comments in the code should describe things that are not obvious from the code itself (“don’t repeat the code in comments” – add design decisions instead)
- Good names are a form of documentation as well (“don’t settle on the first name that roughly describes the thing at hand” – they should be precise and consistent)
- Whenever you change any code, try to improve the design at the same time (“if you’re not making the design better, you likely make it worse”)
- Stick to conventions and try to never change them (“the benefit of consistency almost always outweighs the advantages of a change in conventions”)
Especially the Design it twice imperative resonated with me. I am also observing that developers in my surrounding tend to go for the first solution that pops into their mind. The author gives a very interesting explanation for that: He claims that smart people are conditioned to work this way through their experiences in school and life:
“I have noticed that the design-it-twice principle sometimes is hard for really smart people to embrace. When they are growing up, smart people discover that their first quick idea about any problem is sufficient for a good grade; there is no need to consider a second or third option. This makes it easy to develop bad work habits.”
The author rightfully points out that this strategy stops working if the problems become hard enough. And large scale software systems usually are part of that problem category. So there is a lot to gain from looking at at least two or three fundamentally different designs before starting off.
Comments are essential to good designs
The author claims that comments are essential and should be written upfront as part of the early design process of a class/interface. The core idea is to capture information in the Comments that is not obvious from the code, e.g. design intentions, design decisions, broader ideas, things to keep in mind when calling a method, etc. This makes it also clear that why it’s such a misconception that good code would be “self-documenting”.
Test driven development sets the wrong incentives
One of the more controversial (and thus more interesting) statements of the book is that test-driven development (TDD) is encouraging tactical programming more than strategic programming. It puts the focus on making specific features work, rather than finding the right design. That’s why the author discourages TDD, and only recommends using it when fixing specific bugs that can elegantly be reproduced in a test beforehand, so one can be sure to actually fix them with the change at hand.
The reward for developers
Investing in good software design creates upfront cost. But the cost is worth it for two reasons: - Engineers can spend more time in the design phase (which is actually the fun part of the job) - Engineers will spend less time hunting down bugs that stem from poor design, complicated and brittle code (which is the least fun part of the job)