I recently finished the remarkable book ”A Philosophy of Software Design” by John Outsterhout. This book delves into the intricacies of complexity in software systems and offers insights on how to mitigate or minimize it to a manageable level. The author reflects on the nature of complexity and provides practical advice on combating it, as well as strategies for enhancing system readability, maintainability, and comprehensibility for future developers.
The book is relatively slim, a rarity among programming literature. However, it is packed with valuable insights and practical experiences that will prove beneficial in any programmer’s daily tasks.
It is challenging to create an outline for this book because the information is highly concentrated. Therefore, I’ll just share my thoughts and highlight some of the more interesting ideas from the book. It’s best to keep a paper version of the book as a reference.
From the outset, the author delves into the nature of complexity and identifies its primary symptoms:
- Change amplification: when simple change requires changes in many different places
- Cognitive load: refers to how much a developer needs to know in order to complete a task ,
- Unknown unknowns: when it is not obvious which pieces of code must be modified to complete a task and causes: dependencies and obscurity.
Complexity is incremental, accumulating piece by piece with each small obscurity and new dependency until it becomes unmanageable over time. A system with high complexity is challenging to modify and maintain, requiring increasingly more code with each new change.
Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.
In the next chapter the author goes on to describe two different approaches to software development: Strategic vs. Tactical programming.
Tactical programming is when developer tries to deliver new feature asap without thinking about software design in a high level. Usually such programmer produces a lot of code and implements new features rapidly, sacrificing code quality and adding extra complexity, such system in a long term became a mess. Opposite way is using strategic programming when developer think about problem and well design the solution before starting to implement it. The goal of this programming is to produce a good design first.
In the next chapter, the author describes two distinct approaches to software development: Strategic and Tactical programming.
Tactical programming entails developers aiming to deliver new features as quickly as possible without considering high-level software design. Typically, such programmers generate copious amounts of code and implement new features rapidly, often sacrificing code quality and introducing additional complexity. Consequently, systems developed in this manner often become tangled messes in the long term.
Conversely, strategic programming involves developers carefully considering the problem and designing the solution before initiating implementation. The objective of this approach is to prioritize producing a sound design from the outset.
Strategic programming requires an investment mindset.
This is a common practice in companies with high turnover or a rapid development pace, where each developer focuses on completing their own tasks without considering the overall product design.
Next, the author discusses modular design and offers a definition of a module:
- A software system is a collection of independent modules
- A module can be a class, subsystem, or service
- Each module should be independent of the others
Each module consists of two parts: an interface and an implementation. The interface describes what the module does, while the implementation details how it accomplishes its tasks.
The author then discusses abstractions and emphasizes the importance of crafting effective ones. An abstraction that overlooks crucial details is deemed a false abstraction.
An abstraction is a simplified view of an entity, which omits unimportant details.
If users must read the code of a method in order to use it, then there is no abstraction.
Deep modules
One of the key concepts described by the authors is the notion of deep modules. In essence, deep modules boast a straightforward interface yet encompass a wealth of functionality. In contrast, shallow modules possess a complex interface but offer only trivial functionality.
One of the good examples the author provides in the book is the Unix system call interface, such as write()
or read()
.
Better together or Better Apart?
We often hear that it’s better to split code into smaller pieces to make it clearer and more reusable. However, this isn’t always the best approach. In many cases, it’s better to keep pieces of code together if:
- They share information
- They are used together
- They overlap conceptually
- It’s hard to understand one piece without looking at the other
Each method should do one thing and do it completely
It only makes sense to subdivide a method if:
- The child method is self-sufficient and doesn’t rely on the parent method.
- Someone reading the parent method doesn’t need to understand the implementation of the child method.
Design it twice
The author suggests investing a small amount of time to consider multiple options for each major design decision and then comparing them using the following criteria:
- Does one alternative have a simpler interface?
- Is one interface more general-purpose than another?
- Does one interface enable a more efficient implementation than another?
Why write comments?
Books contains a large portion devoted to comments in the code. Personally, before reading this book I considered a comments as a code smell and tried to avoid them in my code. But this book chaned my mind.
The process of writing comments, if done correctly, will actually improve a system’s design.
“If done correctly” is a key phrase here. Here are some guidelines on how to do it right:
- Comments should describe things that aren’t obvious form the code
- Comments augment the code by providing information in a different level of detail
There are two types of comments: interface comments, which are high-level comments for those who use the abstraction, and implementation comments, which are for those who will maintain the abstraction in the future.
Interface comments provide information that someone needs to know in order to use a class or method. Remember how easy it is to read method descriptions by hovering over them in the code editor, instead of searching through API documentation.
If you want code that presents good abstraction, you must document those abstractions with comments.
If interface comments must also descrivbe the implementation, then the class or method is shallow.
The implementation comments can be used to provide additional information about a class’s methods or properties. For example:
- A method’s interface comment must describe any exceptions that can arise from the method.
- It should outline all preconditions that must be satisfied before a method is invoked.
- Properties’ units, such as milliseconds or meters, should be specified.
The main goal of implementation comments is to help readers understand what code is doing (not how it does it).
Consistency
Consistency is a powerful tool for reducing system complexity and making its behavior more obvious. It can be applied at different levels of a system, such as:
- Names
- Coding style
- Interfaces
- Design patterns
- Invariants
To ensure consistency within the company or team, it’s a good idea to document it and create guidelines to follow. This could be in the form of a Confluence page or Wiki document. Additionally, code static analysis and careful code reviews can enforce adherence to the guidelines.
Red flags
After each chapter, the author provides “red flags,” which highlight patterns that developers should either avoid or minimize.
Conclusion
There are many topics covered in this book, and I’ve highlighted the ones that were most interesting to me. Here’s what I learned after reading it:
-
Code should be clear and understandable to its readers.
Software should be designed for easy of reading, not easy of writing.
-
Deep, general-purpose modules are preferable to shallow, specific ones.
-
Developers should prioritize strategic programming over tactical approaches. In the long term, this reduces system complexity and technical debt.
-
Before splitting a class or method into smaller pieces, careful consideration is essential.
-
Commenting both interface and implementation is beneficial for both writers and readers.
-
Pass-through methods should be identified and avoided.
-
Agile development, with its incremental approach, is beneficial.
-
While Test-Driven Development (TDD) has its merits, it may have more drawbacks than benefits in some cases.
The problem with test-driven development is that it focuses attention on getting specific feature working, rather than finding the best design.
I wholeheartedly recommend this book to every developer seeking to deepen their understanding of software design. John Ousterhout’s insights offer invaluable guidance, empowering readers to apply theoretical concepts to real-world scenarios. By incorporating the principles outlined in this book into their own development practices, developers can navigate complex systems with greater clarity and efficiency. Moreover, the practical red flags highlighted at the end of each chapter serve as invaluable reminders of pitfalls to avoid. Through a combination of theoretical exploration and practical advice, ‘A Philosophy of Software Design’ equips developers with the tools they need to elevate their craft and become more proficient software engineers.