2024 [EN] – A Philosophy of Software Design – John Ousterhout

Fragments of the book, “A Philosophy of Software Design”, written by John Ousterhout.

Preface

The most fundamental problem in computer science is problem decomposition: how to take a complex problem and divide it up into pieces that can be solved independently.

The Nature of Complexity

The ability to recognize complexity is a crucial design skill. It allows you to identify problems before you invest a lot of effort in them, and it allows you to make good choices among alternatives. It is easier to tell whether a design is simple than it is to create a simple design, but once you can recognize that a system is too complicated you can use this ability to guide your design philosophy towards simplicity.

Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system. Complexity can take many forms. For example, it might be hard to understand how a piece of code works; it might take a lot of effort to implement a small improvement, or it might not be clear which parts of the system must be modified to make the improvement; it might be difficult to fix one bug without introducing another. If a software system is hard to understand and modify, then it is complicated; if it is easy to understand and modify, then it is simple.

Complexity is more apparent to readers than writers. If you write a piece of code and it seems simple to you, but other people think it is complex, then it is complex.

Systems of Complexity

  • Change amplification: a simple change requires code modifications in many different places.
  • Cognitive load: how much the developer needs to know in order to complete the task. A higher cognitive load means that developers have to spend more time learning the required information, and there is a greater risk of bugs because they have missed something importanbt. Sometimes an approach that requires more lines of code is actually simpler, because it reduces cognitive load
  • Unknown unknowns: when it is not obvious which pieces of code must be modified to complete a task, or what information a developer must have to carry out the task successfully. It means there is something you need to know, but there is no way for you to find out what it is, or even whether there is an issue. You won’t find out about it until bugs appear after you make a change. Change amplification is annoying, but as long as it is clear which code needs to be modified, the system will work once the change has been completed. Similarly, high cognitive load will increase the cost of a change, but if it is clear which information to read, the change is still likely to be correct. With unknown unknowns, it is unclear what to do or whether a proposed solution will even work.

One of the most important goals of good design is for a system to be obvious.

Working Code Isn’t Enough

The most important thing is the long-term structure of the system. Strategic programming requires an investment mindset. Rather than taking the fastest path to finish your current project, you must invest time to improve the design of the system. These investments will slow you down a bit in the short term, but they will speed you up in the long term.

When you discover a design problem, don’t just ignore it or path around it; take a little extra time to fix it. This is the opposite of tactical programming, where you are continually adding small bits of complexity that cause problems in the future.

Modules Should be Deep

In order to identify and manage dependencies, we think of each module in two parts: an interface and an implementation. The interface consists of everything that a developer working in a different module must known in order to use the given module. The implementation consists of the code that carries out the promises made by the interface.

The best modules are those whose interfaces are much simpler than their implementations. Such modules have two advantages. First, a simple interface minimizes the complexity that a module imposes on the rest of the system. Second, if a module is modified in a way that does not change its interface, then no other module will be affected by the modification.

Abstractions

An abstraction is a simplified view of an entity, which omits unimportant details.

In modular programming, each module provides an abstraction in the form of its interface. The more unimportant details that are omitted from an abstraction, the better. However, a detail can only be omitted from an abstraction if it is really unimportant. An abstraction can go wrong in 2 ways:

  • it can include details that are not really important: this increases the cognitive load on developers using the abstraction
  • it can omit details that are really important: this results in obscurity.

Information Hiding (an Leakage)

Order usually does matter, so it will be reflected somewhere in the application. However, it shouldn’t be reflected in the module structure unless that structure is consistent with information hiding. When designing modules, focus on the knowledge that’s needed to perform each task, not the order in which tasks occur.

In Temporal Decomposition, execution order is reflected in the code structure: operations that happen in different times are reflected in different methods or classes. If the same knowledge is used at different points in execution, it gets encoded in multiple places, resulting in information leakage.