What makes a certain software design better than any other? In A Philosophy of Software Design, John Ousterhout offers a simple but profound idea: the goal of software design should be to minimize complexity as much as possible, with complexity being defined as “anything related to the structure of a software system that makes it hard to understand and modify the system”. It’s as simple as that! But why should this be our goal? As the author explains, as a system grows and complexity accumulates, “it becomes harder and harder for programmers to keep all of the relevant factors in their minds as they modify the system. This slows down development and leads to bugs, which slow development even more and add to its cost.”.
I think this is extremely well put, and I love this definition of complexity because it’s entirely centered around human beings. I think it can be easy for engineers—but especially for managers!—to focus more on the objective aspects of the systems we work on: whether they work as specified, whether they’re performant, etc. But we can’t ignore this critical human idea of complexity, because if left unchecked, creeping complexity will gradually slow down the speed at which we can fix bugs and add features within any given system, correspondingly increasing developer frustration. Slower development time means greater costs, and increased frustration means higher developer turnover, so minimizing complexity should be something everyone at our organizations, from developers all the way to CEOs, should be concerned with.
The author builds on this simple definition of complexity to define the complexity of an overall system as the sum of the complexities of each part, weighed by the fraction of time that developers spend working on that part. At first glance it didn’t seem to me like that definition was an accurate representation of what most developers actually mean when they say that a system is complex. But on further thought, we wouldn’t for instance consider an otherwise simple system that makes use of a modern relational database as “complex”. Yes, the database system itself is extremely complex, but since we don’t need to peek inside and contemplate its inner workings in order to make use of it, its complexity has essentially been trapped; therefore the overall system certainly feels simple. So on further thought, I think this definition is actually brilliant, and an incredibly useful way of thinking about the complexity of a system and the quality of its design. Reduce this measure for a given system and it will become easier, more pleasant, and more efficient to maintain.
With this powerful definition of complexity in hand, the rest of the book looks at various ways that complexity can creep into our software systems and offers specific tips on how to fight these various manifestations. I think my single favorite one of these might be the idea of deep versus shallow modules: deep modules hide a lot of functionality behind a relatively small interface, and shallow modules are the opposite. The author explains that “Module depth is a way of thinking about cost versus benefit. The benefit provided by a module is its functionality. The cost of a module (in terms of system complexity) is its interface.” I never thought of interfaces as adding complexity to our systems, but of course it’s obviously true: interfaces need to be studied to be understood and made useful, just like any other parts of our systems. And what this means overall is that deep modules should be preferred over shallow ones. In some cases, the cost of a module, its interface, might even be outweighed by its benefit, the complexity it hides, meaning it probably shouldn’t exist at all! It also means that we shouldn’t feel compelled to automatically start slicing up longer functions or modules into smaller pieces just because of their length. If they’re long but still feel rather easy to understand, we might make them overall more complex by slicing them up, since every new piece now comes with the burden of an interface that adds complexity to the overall product.
Here’s a nice little chart from the book that illustrates this idea of deep versus shallow modules:
There’s a lot of other great ideas and tips to reduce complexity discussed throughout the book, for instance:
- Chapter 3 discusses the difficult problem of knowing when you should be focusing on design work (”strategic programming”) versus when you should be focusing on just getting something to work (”tactical programming”). I love and wholeheartedly agree with the following quote from this chapter: “the ideal design tends to emerge in bits and pieces, as you get experience with the system. Thus the best approach is to make lots of small investments on a continual basis”.
- Chapter 5 argues that the most important technique to achieve deep modules is “information hiding”, which is knowledge that is “embedded in the module’s implementation but does not appear in its interface, so it is not visible to other modules”. The part about information not being visible to other modules is critical, because if the information isn’t hidden, the complexity hasn’t actually been trapped. This can manifest itself in what the author calls “information leakage”, where information within a module is actually duplicated outside of it, contributing to complexity. I think these ideas of trying to hide as much information as possible within our modules and of preventing information leakage are highly useful to keep in mind while designing software.
- Chapter 11 promotes the interesting idea of considering multiple designs for each major decision: “you’ll end up with a much better result if you consider multiple options for each major design decision”. It’s not something I have the habit of doing, but I can see how it would help produce better designs: I’ll have to give it a try!
- I love Chapter 12 because it provides a rebuttal to common excuses for not writing comments, excuses that I’ve heard myself many times before. I agree with the author that comments can make systems much easier to understand and therefore maintain, and that they’re worth investing in.
- The title of Chapter 13 itself is a simple yet powerful idea we should always keep in mind when writing comments: “comments should describe things that aren’t obvious from the code”.
- Chapter 14 is interesting because it discusses the critical problem of choosing names for identifiers! Of course choosing names is an important aspect of reducing complexity, since good names can obviously make our systems easier to understand. But unfortunately it’s notoriously hard to do, as the famous quote attributed to Phil Karlton says: “There are only two hard things in Computer Science: cache invalidation and naming things". So it’s nice to have a chapter dedicated to this topic.
There are a few pieces of advice in the book I didn’t necessarily agree with though. For instance, in Chapter 15 the author recommends that you write your comments at the beginning of the design process, before writing any code. I just can’t see myself following this advice. It reminds me of test-driven development, where developers are encouraged to write tests before any implementation code, which I also don’t think is practical or useful most of the time. I think writing a good description of a design is a difficult and time consuming activity, and in practice as developers we need to actually write some code and experiment with our design ideas to see if they even work. In other words, our designs should probably evolve quite a bit before we settle on something we’re willing to commit to (this idea of experimenting with multiple design options is even recommended in the book itself, in Chapter 11, as I mentioned earlier). I think it would be quite inefficient if we had to write and evolve our comments every time we changed our design in the early experimental phases: better, in my opinion, to wait until things have settled down a bit.
Apart from that, I think the book would benefit from a broader discussion on automated testing and how it might affect the design process. It does devote a chapter to the tension between performance and complexity, yet a similar tension clearly exists between testability and complexity. Considering how important automated testing is to modern software development, a chapter on the topic would be useful.
Overall, A Philosophy of Software Design offers a powerful mental model and a North Star for guiding our software design decisions. I highly recommend it to software developers at any level of experience.
Top comments (0)