Not My Philosophy of Software Design
It’s been a long time since I wrote a detailed book review. I write book reviews when I am filled with emotions after reading, whether positive, negative, or mixed. This article is a rather angry review of the book I recently read named “A Philosophy of Software Design” by Dr. J. Ousterhout. Why angry? Well, because this book basically defies all the best practices and principles of software design and development that I have acquired in the course of my more than 20 years as a software engineer.
Stay tuned and I will explain why.
Introduction
The author starts with logical claims that the complexity of software is a constantly and unavoidably increasing measure as a program grows and effectively grows the number of people who work on it. Although it is impossible to prevent complexity growth, it is possible to delay and slow it down through a simpler design. What are the ways to reduce complexity? First, by excluding handling of special cases and “using identifiers in a consistent fashion,” states the author. I am not entirely sure why these two measures go together, and while I agree that identifiers must be used in a consistent fashion, I would challenge the thesis about special cases. The second approach advocated by Dr. Ousterhout is modular design, which is understood as complexity encapsulated in separate modules.
Overall, Prof. Ousterhout concludes that there isn’t a simple recipe that will guarantee great software design. The purpose of the book, therefore, is to provide a set of ideas and concepts that, according to the author, should simplify the design challenge.
Chapter 2
Chapter 2 starts with a definition of what complexity is. The emphasis of the given definition is on understanding and (hence?) modification of the system. In fact, the definition provided in the book is just hiding the problem behind semantic twisting. Defining complexity as the measure of how hard it is to understand or modify something is just saying that complexity is how complex it is to understand or modify something. The mathematical definition of the complexity, given a few paragraphs later, is no better, because it defines the overall complexity through the complexity of each system’s part. Well… it’s all turtles all the way down…
One thing the author rightfully points out is that complexity is easier for readers to detect and measure than for writers. Here would be a great place to emphasise the importance of code review based on this, but alas, this point is not raised…
In the next section the book speaks about the symptoms of complexity and three are outlined:
- change amplification,
- cognitive load,
- unknown unknowns.
I liked the “unknown unknowns”, which is basically some ephemeral measure of how difficult for a developer to grasp what parts of code have to be modified in order to introduce a new feature or a change. The only remark about this “symptom” is that it is, to my mind, essentially the same as the “cognitive load” one. The real problem is that the author doesn’t provide a solution for this problem, which, from my experience, lies on the surface: to know which pieces of code to modify one should start with either brand-new automated acceptance tests or explore the existing ones if the task is about modifying the existing functionality.
The chapter concludes with the statement that the major cause of complexity is the number of dependencies (there is another one mentioned — obscurity, but I won’t focus on that). This will be an important thing as the book progresses, so for now let’s just keep this statement in mind.
Chapter 3
Chapter 3 focuses on the importance of small investments as a concept of strategical programming. I agree basically with everything said in that chapter, but I feel that one thing is massively missing: what exactly should one invest in? I would stress that these small investments should be accurately written automated tests created upfront…
Chapter 4
And here we finally approach what I consider one of the most controversial and arguable points of Dr. Ousterhout’s book: the concept of module depth. More specifically the author advocates the thesis that modules (e.g. classes) should be deep. What this concept means is that a deep module provides a lot of functionality through a simple interface. The notion of simplicity is not clearly defined, so we come to the turtles all the way down again, but that is another story.
As an example of a shallow, i.e. not a deep module, the book criticises the Java I/O classes such as FileInputStream
. Across the book this criticism of Java’s I/O library’s architecture is mentioned a few times and the whole claim boils down to the idea that FileInputStream
should encapsulate a buffered input stream inside it by default, which can be deactivated with a special flag.
If you didn’t get what it means, here is an example (rewritten with try-with-resources, not like in the book). Instead of having two separate classes as FileInputStream
and BufferedInputStream
and utilising them in the conventional way as
try (BufferedInputStream buffInputStream = new BufferedInputStream(new FileInputStream(filename))) {
...
}
from what I understood, John Outsterhout basically suggests simply
try (FileInputStream inputStream = new FileInputStream(filename)) {
...
}
and if a developer needs for some reason a non-buffered input stream, it would be something like
try (FileInputStream nonBuffInputStream = new FileInputStream(filename, false)) {
...
What’s wrong with the idea of deep vs shallow classes?
First, I want to play a role of a Java advocate and defend its I/O library architecture. There are plenty of cases where one does not need a buffer. I won’t proliferate on this particular topic (a curious reader will search for examples by themselves), and the author actually mentions this type of argument and suggests a special constructor that disables buffering. Now, if we imagine that FileInputStream
were designed like that, it would be a terrible idea, because:
- its name would need to be changed to
FileBufferedInputStream
to reflect the fact that it is a buffered stream. But then it ruins the whole idea of customising its behaviour by optionally switching the buffering off; - how would a design of such a library where we want to re-use buffering in different streams look like? We would end up either with an ugly utility class implementing buffering, or stay with the same
BufferedInputStream
and get a bunch of duplicated logic in many different streams that, according to Dr. Ousterhout, should offer buffering by default; - finally, such a design would break the Single-responsibility principle, because, well, a file input stream should do its job of reading a file properly, whereas buffering is a completely different story.
In fact, this very example of Java I/O library, shows why this sort of obsession with deep classes, offering a lot of functionality, is somewhat impractical, odd and bizarre, leading to misleading identifiers and code duplication.
But that’s not only this. While reading the book a natural question arises: are REST controllers shallow classes? It seems that they are, if we follow the book’s concept. I guess, in the best of the author’s worlds, let’s say a Spring REST controller would do all the job: business logic and data access, without any additional service and persistence layers’ classes. Again, I leave the reader to contemplate on this subject, just giving a flavour of what potentially is wrong with the idea of deep vs shallow classes.
Last, but not least. If we follow Domain-Driven Design concept, our classes would represent specific domain’s abstractions. And it can lead to the emergence of lots of shallow modules. Why? Here we jump to…
Chapter 6
…where the author says that general-purpose modules are deeper and hence preferable.
This approach has three problems.
First, when we speak about general purpose classes, there is a risk of Premature generalisation. Akin to Premature optimisation, an attempt to make things more abstract is an immature malpractice that leads to time and effort waste, and as a result, bad design, hard to maintain and comprehend code. One of the things that unites general purpose classes with “deep” classes is they tend to be overloaded with functionality and, from the interface perspective, be overloaded with parameters (see the custom constructor for a putative “deep” version of FileInputStream
suggested above). Curiously enough, it contradicts the core idea of the book, which is to have a simple interface with a deep implementation.
Second, having general purpose modules (or classes) goes against the Composition over inheritance principle. General purpose classes are hard to reuse in a composition way. They create unnecessary coupling, bring extra functionality and cause logic or code duplication. See the problem with the “embedded” streaming functionality we discussed above.
Finally, and this is why I mentioned DDD above, general purpose classes have no correspondence to the real world. If we adhere to the DDD principles, our classes should have a direct correspondence to the domain models. One way or another we will inevitably create shallow modules.
When reading Chapter 6, my mind screams with keywords like YAGNI. The idea of how potentially useful a general-purpose interface could be dominates this topic in the book and tells me a lot about this specific philosophy of software design. If you don’t want to swamp in an infinite process of creating abstractions, make refactoring, maintenance and code comprehension awfully terrible, then don’t do that, don’t shoot yourself in the foot: go, create shallow classes as soon as they help you to make your code self-explanatory, as soon as they correspond to your domain’s entities. There is nothing wrong with it. On the contrary, deep classes is a nightmare for future maintenance. They bring nothing, but a satisfaction to immature developers, whose passion is exercising in making things abstract.
Chapter 7
Everything that’s being said above about deep and shallow modules applies equally to the concept of “pass-through methods” revealed in Chapter 7. It is essentially the same idea applied on the procedural level. Of course, the author criticises “pass-through methods” because they “contribute no new functionality”. Again, we should then throw away REST controllers, for example, because apart from annotations defining the REST API specifications, these methods often do nothing else but model mapping and calling business logic from the service layer.
Chapter 9
…is the one that made me shout “Aaaa!!!” a few times:
- it advocates some use of the
GOTO
statement (where this statement is available on the language level); - if offers awful code snippets, returning
null
from catch blocks; - it states that it’s okay to have methods containing hundreds of lines as soon as their signature is simple (Gosh, what is “simple” anyway?);
- in the same stream of thinking the author suggests joining “shallow” methods together to make them “deeper”;
- finally this chapter contains an honest confession from the author that his philosophy is somewhat opposite one to Uncle Bob’s one, baptised as Clean Code.
Chapter 12
Skipping an overview of chapters 10 to 11, chapter 12 made me write “WT#” and “???” comments on the book’s fields. In this chapter J. Ousterhout stands against the idea of self-documenting code. He states that “comments are fundamental to abstractions”, and another argument of his against self-documenting code is that it leads to a large number of shallow methods. Surely it does!
This is a bright example of a casual dependency fallacy: the fact that in order to make code self-explanatory we have to create many shallow modules doesn’t mean we shouldn’t do that, just because someone thinks that shallow modules (or methods) are a bad idea.
The wishful thinking of the author that he “debunked” the arguments against writing comments is followed by a thesis, that comments somehow capture the information that the software engineer couldn’t represent in the form of code. Just think about it. My first question is: why do we need this information in the form of a comment at all? I can think of one example of such information, this would be a specific acceptance criteria from a user story. But then, they should be represented in the code in the form of automated tests.
Same reaction as above I had while reading Chapter 18. The author basically challenges the Dependency Inversion Principle by saying that we should not declare variables by using their interface, but should use the actual variable type instead. I.e. instead of
List<String> list = new ArrayList<>();
the author suggests
ArrayList<String> list = ...
I leave the readers with this wow revelation without any additional comments.
Chapter 19
unleashes criticism of Test-Driven Development. One of the principal arguments that the author uses against TDD is that TDD focuses on specific features development rather than finding the best design. In response to that I would like to remind that software development is primarily about satisfying the business needs in the first place, and not about academic competition in finding the best design. But of course, the best design comes naturally from the best practices. More about it you can find in my previous article: How good architecture emerges from following the best engineering practices.
Conclusion (my conclusion, not the book’s conclusion chapter)
This book was a great surprise for me. It honestly and openly contradicted the Clean Code philosophy, the TDD principles, the SOLID principles, the Domain-Driven Design concept and all my past experience in general. Unfortunately I cannot recommend this book to anyone.