Skip to main content

Thoughts on "A Philosophy of Software Design"

I took a recommendation from the blog of Gergely Orosz on a recently released book on software architecture, "A Philosophy of Software Design" by John Ousterhout, purchased and have just finished reading it. In short, I'd second the recommendation - it's not a long book, but is packed with good, practical advice presented from the perspective of an academic, mostly based on his views of many implementations of projects by his students.

What I particularly enjoyed about it was that it's a book that makes you think as you go... you aren't simply ticking off a list of things you agree with already. Gergely had a few reservations that he outlined in his post, most of which I also agree with, and I also had some of my own - but all in a postitive way as it made me think and question some practices I've used up to now and consider them from a different viewpoint.

In the appendix, John lists 15 design principles that he describes fully in the book, which I'll list below as a taster, and add my own brief thoughts on them, based on how they are presented in the book.

Design principles

1) Complexity is incremental: you have to sweat the small stuff

The point here being that it isn't one major change that moves an application away from being maintainable and comprehensible, it's the culmination of many decisions, short-cuts and acceptance of "tactical" modifications that can lead to this situation. This fits nicely with the idea of technical debt, a build-up of minor issues that in the end will need to be paid off to avoid slowing down feature development.

2) Working code isn't enough

Taking the time and effort to ensure that a given solution to a requirement doesn't just work, it works right... is consistent, fits with the overall design, isn't a compromise etc.

3) Make continual small investments to improve system design

Gergely raised a criticism of the book in not mentioning the topic of testing - which wasn't completely accurate as it does get a look-in in chapter 19 - but he still makes a good point that this aspect could well have been incorporated into the main narrative of the book when discussing good design. Partly as testable code generally is well designed, and partly as it's the safety of a comprehensive unit test suite that allows for these continual improvements to refactor working code to a better design.

4) Modules should be deep

John's point here being that the preference should be for modules that provide powerful functionality yet have a simple interface. This is probably the one that got me thinking the most, as it sounds good, but he also contrasts this with the conventional wisdom of classes being small. An adherence to the single responsibility principle (SRP) for a class will likely tend it to be smaller, as different responsibilities are broken out into different classes. For me I think this circle is squared by considering a module not necessarily as a single class, rather a piece of significant functionality exposed via a simple interface, that may well be implemented by a class that itself depends on other classes for specific parts of it's features.

5) Interfaces should be designed to make the most common usage as simple as possible

Sensible advice to limit the options available to the client's of a given module - whether that be a low-level method call or a higher-level API - exposing the ability to customise the usage and responses as needed, but ensuring that sensible defaults are available.

6) It's more important for a module to have a simple interface than a simple implementation

Asking the writer of a module to "pull complexity downwards", i.e. take on the responsibility of making a module as easy as possible for users of it, even if that means taking on more work yourself. Calling code doesn't need to know the details of the implementation in order to be able to use it, just how to navigate the interface.

7) General purpose modules are deeper

This is another one that rubs up against a commonly agreed sensible principle, that of YAGNI ("you ain't gonna need it"), where you are advised to build just what is needed for the current requirements, and not try to predict what might be needed in future-proofing your work. Certainly a gold-plated, generic solution to a problem that doesn't yet exist should be avoided, but I agree with John's pragmatic view of making modules "somewhat general purpose", where he particularly focuses on looking to make a more general purpose interface, even if the current implementation is quite specific.

8) Separate general-purposes and special-purpose code and 9) Different layers should have different abstractions

Good advice on separating components vertically, seemingly applicable to that suggested by clean architecture, with higher-level, more abstract component containing logic used by other components dedicated to specific user interfaces or infrastructure.

10) Pull complexity downward

As per 6).

11) Define errors out of existence

This was an interesting one where John exhibits a strong aversion for exceptions. His point wasn't so much around the principle of avoiding use of exceptions to control program flow, but more looking to ensure exceptions aren't thrown when they really don't need to be. An example might be deleting a file that doesn't exist - this could seemingly just fail silently with the same result as if a file that did exist was deleted - which seems sensible advice and chimes with his other points about simplifying interfaces for callers.

There's a limit to this though; genuine exceptions are better allowed to occur, be monitored, logged and acted on.

12) Design it twice

This pointer was a good one, and something I'm going to try to bear in mind moving forward. The idea is that when considering how to implement a given feature, don't just go with the first thing that comes to mind, even if it seems the obvious approach. Take some time to consider alternate designs, which, even if you still go with the first idea, may still help flush out some downsides and compromises that will need to be handled.

13) Comments should describe things that are not obvious from the code

John's views on comments may at the outset seem a little controversial, in that he's very much in favour of them, against what I see to be a prevailing trend of considering a comment as a sign that come code isn't as clear as it should be, and could be refactored to improve naming, appropriate responsibilities or other aspects of comprehensibility. There is a danger though in taking this too far. Writing comments may not be the most fun task for most programmers, and so there can be a tendency to use this as a crutch to avoid writing them. Personally I do find a consistent approach to class and method headers valuable, and, in many cases, inline comments describing the "why" of given code constructs, essential.

14) Software should be designed for ease of reading, not ease of writing

This aligns with other common observations such as code being read more than it's written, and hence, in any choice between approaches, favour what benefits the reader over the writer. The example John uses has a correlate with key value pairs or tuples in C#, which, whilst convenient as the result type of a method, I usually find once I've used one that I'll want to refactor it to a class construct later.

15) The increments of software development should be abstractions, not features

The last sacred cow John takes a critical eye over is agile development, not to go against the project management approach as a whole of course, but just to flag the danger that small, incremental updates may lead to a tactical, feature driven development pattern that doesn't leave room for proper design consideration, and may drive up overall complexity over time. His solution here is to retain incremental development, but focus on the abstractions, not just features. I'm not sure I quite followed his point exactly here, but this chimes with my experience in that there's a need for technical leadership to ensure that stories that include refactoring work are prioritised in backlogs along with product owner driven feature requests.

Conclusions

Overall, I'd be happy to recommend this book to anyone interested in how we build software. I liked the humble approach John took - he stresses that he won't have all the answers, but he has some views based on significant experience that he shares, and would like feedback. It's an easy read, not too long and with short chapters, but with plenty to chew on as you go.

Comments