A Philosophy of Software Design is a book written by John Ousterhout, a
well-accomplished Standford University professor, who’s also known for creating
the Tcl scripting language and the Raft consensus algorithm.

Here’s a summary of the ideas in the book:
The nature of complexity
- Complexity is anything related to a system that makes it hard to understand
(cognitive load) or difficult and risky to modify (change amplification)
- Complexity is incremental through the accumulation of dependencies and obscurity.
Working code is not enough
- Tactical programming (adding complexity in order to finish fast) is not acceptable.
- Continually investing in good design pays for itself eventually. On the other
hand, delaying the investment makes it increasingly more intimidating and painful.
Modules should be deep
- Interfaces should be deep, not shallow.
- An interface that is simpler than its implementation maximizes the hidden
complexity and is therefore the effort of learning for its users.
- Interfaces should be designed to make the most common use case easy to achieve.
- Information hiding is closely related to deep interfaces.
- Temporal decomposition (based on when operations happen in runtime) is a
source of information leakage, and thus complexity.
- Design should therefore focus on the knowledge needed to perform the task,
not when it is performed.
- Merging classes (up to a certain point) can sometimes improve information
hiding, when multiple smaller classes share a lot of information.
General-purpose modules are deeper
- Aim for the simplest interface that covers all your needs, while still easy
to use and is adequate to use in as many situations as possible.
- Special-purpose interfaces are shallow, and tend to leak information and make
the system more complex.
Different layer, different abstraction
- Each design piece added to a system (interface, argument, class etc,.) should
aim to reduce complexity by encapsulating functionality so that users don’t
need to learn about it for instance.
- Pass-through methods are shallow and add no functionality, and are a sign of
lack of clean division of responsibility between classes.
- Classes should be refactored to disentangle pass-through method, expose
that lower-layer or merge it into the calling class.
Pull complexity downwards
- A simple interface is more important than a simple implementation.
- Take a bit more suffering on yourself, to reduce suffering for others.
Better together, or better apart ?
- Modules should be merged or split based on what produces the deepest
interfaces, the best information hiding and the fewest dependencies.
- Each method should do one thing and do it completely.
- Methods should be understandable independently of the implementation of other
methods.
Define errors out of existence
- Classes with lots of exceptions are shallower. The number of places where
exceptions have to be handled should be reduced.
- The semantics of an interface could be redefined to eliminate error
conditions. Alternatively, errors can be masked at lower-levels or aggregated
into more generic handlers.
- If acceptable, just crashing could be conceivable.
Design it twice
- The process of designing and comparing multiple approaches and ideas will
lead to finding simpler, more efficient and general-purpose interfaces. It
will also improve your design skills over time.
- Writing good documentation can improve a system’s design, clarify
dependencies and reduce obscurity and therefore reducing complexity.
This should be approached with an investment mindset.
- Comments should capture the ideas in the designer’s mind that aren’t present
in code.
- If users must read a method’s code before using it, then there’s no
abstraction.
- Keeping documentation updated doesn’t require larger efforts than updating
the related code.
- It is important to pick a convention for commenting (what to comment, format etc,.)
- Comments should augment the code by providing information at a different
level of detail, instead of just repeating it. Using different words is a first step.
- Lower-level comments add precision, such as what a variable represents,
instead of how it is manipulated.
- Higher-level comments improve intuition, by abstracting what the code does
and the reasons behind it.
- Interface comments should describe the usage. Having to describe the
implementation details, is a sign of shallow abstractions.
- Implementation comments should describe what the code is doing (and why), not
how it does it.
Choosing names
- Choosing good names is very underrated. Vague, inconsistent or imprecise
names could lead to hard to track bugs and more obscurity.
- Hard to pick names are a sign of bad design.
- Writing documentation at the beginning improves its quality, helps improving
the design, and captures better the designer’s intentions.
- Writing comments early is more enjoyable, and less of a drudge.
*Modifying existing code
- Programmers usually make the smallest change that would achieve their goal.
Such a change should instead aim to give the system the (better) design it
would have had if they were re-building it with the current knowledge.
- Comments belong in the code, close to what they describe, and certainly not in the commit log.
- Avoid comments duplication by making references.
- High-level comments that don’t repeat the code tend to be easier to maintain.
Consistency
- Consistency can be ensured through documents that list the most important
conventions, tooling that enforces the checks, and not changing the existing
conventions, unless really needed.
- Consistency is an investment mindset. However, taking it too far can create
confusion and complexity.
Code should be obvious
- The code’s meaning and behaviour should be obvious from a quick read.
Precise, meaningful naming and consistency reduce contribute to this.
- Judicious use of white space and comments contribute to making the code
easier to read.
- Event-driven programming, generic containers and violating the reader’s
expectations can make the code harder to read.
Software trends
- Mechanisms provided by object-oriented programming do not necessarily protect
from design mistakes such as shallow classes and complex interfaces.
- Developing incrementally as advocated by Agile is usually a good idea.
However, it should be applied to abstractions, not features.
- The issue with Test-Driven Development is that it focuses on making specific
features working, rather than finding the best design.
- Software development paradigms should be judged based on whether they helping
minimizing the overall complexity of the system.
- Clean design is compatible with high performance. Simple code tends to be
fast enough as it eliminates redundant work, and makes it easier to find and
optimize the critical paths.