Skip to content

A Philosophy of Software Design


  • Author: [[John Ousterhout]]


The first symptom of complexity is that a seemingly simple change requires code modifications in many different places. — location: 328 ^ref-63737

The second symptom of complexity is cognitive load, which refers to how much a developer needs to know in order to complete a task. — location: 335 ^ref-31034

The third symptom of complexity is that 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. — location: 351 ^ref-65144

Complexity is caused by two things: dependencies and obscurity. — location: 370 ^ref-38878

a dependency exists when a given piece of code cannot be understood and modified in isolation; the code relates in some way to other code, and the other code must be considered and/or modified if the given code is changed. — location: 372 ^ref-12876

The second cause of complexity is obscurity. Obscurity occurs when important information is not obvious. A simple example is a variable name that is so generic that it doesn’t carry much useful information — location: 390 ^ref-63133

3.3    How much to invest? So, what is the right amount of investment? A huge up-front investment, such as trying to design the entire system, won’t be effective. This is the waterfall method, and we know it doesn’t work. The ideal design tends to emerge in bits and pieces, as you get experience with the system. Thus, the best approach is to make lots of small investments on a continual basis. I suggest spending about 10–20% of your total development time on investments. — location: 461 ^ref-42273

Good design doesn’t come for free. It has to be something you invest in continually, so that small problems don’t accumulate into big ones. Fortunately, good design eventually pays for itself, and sooner than you might think. — location: 507 ^ref-6765

One of the most important techniques for managing software complexity is to design systems so that developers only need to face a small fraction of the overall complexity at any given time. This approach is called modular design, and this chapter presents its basic principles. — location: 516 ^ref-51300

The best way to use this book is in conjunction with code reviews. When you read other people’s code, think about whether it conforms to the concepts discussed here and how that relates to the complexity of the code. It’s easier to see design problems in someone else’s code than your own. You can use the red flags described here to identify problems and suggest improvements. Reviewing code will also expose you to new design approaches and programming techniques. — location: 274 ^ref-8046

This book is about how to design software systems to minimize their complexity. — location: 293 ^ref-13571

Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system. — location: 305 ^ref-35742

You can also think of complexity in terms of cost and benefit. In a complex system, it takes a lot of work to implement even small improvements. In a simple system, larger improvements can be implemented with less effort. — location: 309 ^ref-4181

2.4    Complexity is incremental Complexity isn’t caused by a single catastrophic error; it accumulates in lots of small chunks. A single dependency or obscurity, by itself, is unlikely to affect significantly the maintainability of a software system. Complexity comes about because hundreds or thousands of small dependencies and obscurities build up over time. Eventually, there are so many of these small issues that every possible change to the system is affected by several of them. — location: 402 ^ref-62770

Most programmers approach software development with a mindset I call tactical programming. In the tactical approach, your main focus is to get something working, such as a new feature or a bug fix. — location: 422 ^ref-8378

The tactical tornado is a prolific programmer who pumps out code far faster than others but works in a totally tactical fashion. When it comes to implementing a quick feature, nobody gets it done faster than the tactical tornado. — location: 441 ^ref-51383

Consider a module that implements balanced trees. The module probably contains sophisticated code for ensuring that the tree remains balanced. However, this complexity is not visible to users of the module. Users see a relatively simple interface for invoking operations to insert, remove, and fetch nodes in the tree. To invoke an insert operation, the caller need only provide the key and value for the new node; the mechanisms for traversing the tree and splitting nodes are not visible in the interface. — location: 532 ^ref-52134

order to 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 know in order to use the given module. — location: 527 ^ref-26922

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. — location: 541 ^ref-2479

The informal parts of an interface include its high-level behavior, such as the fact that a function deletes the file named by one of its arguments. — location: 551 ^ref-6642

The term abstraction is closely related to the idea of modular design. An abstraction is a simplified view of an entity, which omits unimportant details. — location: 559 ^ref-65203

The best modules are those that provide powerful functionality yet have simple interfaces. I use the term deep to describe such modules. — location: 582 ^ref-11481

Interfaces are good, but more, or larger, interfaces are not necessarily better! — location: 593 ^ref-15192

The extreme of the “classes should be small” approach is a syndrome I call classitis, which stems from the mistaken view that “classes are good, so more classes are better.” — location: 644 ^ref-25327

Providing choice is good, but interfaces should be designed to make the common case as simple as possible — location: 664 ^ref-55222

If an interface has many features, but most developers only need to be aware of a few of them, the effective complexity of that interface is just the complexity of the commonly used features. — location: 672 ^ref-17953

The most important technique for achieving deep modules is information hiding. This technique was first described by David Parnas1. The basic idea is that each module should encapsulate a few pieces of knowledge, which represent design decisions. The knowledge is embedded in the module’s implementation but does not appear in its interface, so it is not visible to other modules. — location: 685 ^ref-39220

When designing modules, focus on the knowledge that’s needed to perform each task, not the order in which tasks occur. — location: 742 ^ref-41715

Defaults illustrate the principle that interfaces should be designed to make the common case as simple as possible. They are also an example of partial information hiding: in the normal case, the caller need not be aware of the existence of the defaulted item. In the rare cases where a caller needs to override a default, it will have to know about the value, and it can invoke a special method to modify it. — location: 821 ^ref-17486

When decomposing a system into modules, try not to be influenced by the order in which operations will occur at runtime; that will lead you down the path of temporal decomposition, which will result in information leakage and shallow modules. Instead, think about the different pieces of knowledge that are needed to carry out the tasks of your application, and design each module to encapsulate one or a few of those pieces of knowledge. This will produce a clean and simple design with deep modules. — location: 847 ^ref-17041