A Philosophy of Software Design - John Ousterhout
This is still a draft. I hope to finish the book in a few days, and then do a summary. But I will collect my thoughts here until then. ๐
Chapter 1 - It's all about complexity
-
Complexity will still increase over time, in spite of our best efforts, but simpler designs allow us to build larger and more powerful systems before complexity becomes overwhelming
-
There are genrally 2 approaches to managing complexity:
- eliminate it by making code simpler and more obvious. e.g by eliminating special cases
- modular design: encapsulate it, so that programmers can work on a system without being exposed to all of its complexity at once.
-
Incremental design means that software design is never done. Design happens continuously over the life of a system. Incremental development means continuous redesign.
-
As a Software Developer, you should always be on the lookout for opportunities to improve the design of the system you are working on, and you should plan on spending some fraction of your time on design improvments.
Chapter 2 - The nature of complexity
-
Complexity: 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. Your job as a developer is not just to create code that you can work with easily, but to create code that others can also work with easily.
-
Symptoms of complexity:
- Change amplification: The first symptom of complexity is that a seemingly simple change requires code modifications in many different places. One of the goals of good design is to reduce the amount of code that is affected by each design decision, so idesign changes don't require very many code modifications.
- Coginitive load: how much a developer needs to know in order to complete a task. Sometimes, an approach that requires more lines of code is actually simpler, because it reduces congnitive load.
- Unknown unknowns: there is something for you to know, but there is not way to find out what it is, or whether there is an issue. You won't find out until bugs appear after you make a change.
-
One of the most important goals of good design is for a system to be obvious. An obvious system is one where a developer can make a quick guess about what to do, without thinking very hard, and yet be confident that that guess is correct.
Chapter 3 - Working code isn't good enough
(Strategic vs. Tactical Programming)
-
Tactical mindset: A Tactical mindset is focused on getting features working as quickly as possible. This is what most organisations encourage. However, a more strategic approach is necessary if you want a good design. With tactical mindset, you:
- are trying to finish a task a quickly as possible.
- planning for the future isn't a priority.
- don't spend time looking for the best design
- tell yourself it's ok to add a bit of complexity if it allows the current task to be completed quickly.
-
Complexity: it is not one particular thing that makes a system complicated, but the accumulation of dozens of hundreds of small things. If you program tactically, each programming task will contribute a few of these compleexities. Each of them probably seem like a reasonable compromise in order to finish the current task quickly. However, the complexities accumulate rapidly, especially if everyone is programming tactically.
-
Tactical tornado: a prolific programmer who pumps out code faster than others but works in a totally tactical fashion. Nobody implements a feature faster than a tactical tornado. Some organisations treat them as heroes. Other engineers must clean up the mess they left behind. Real heroes make slower progress than the tactical tornado.
-
Strategic programming: First step to becoming a good software designer is to realize that working code isn't enough. It is not acceptable to introduce unnecessary complexities in order to finish your current task faster. The most important thing is the long-term structure of the system.
- Your most important job as a Developer is to facilitate future extensions of existing code; since most of the code in any system is written by extending the existing code.
- Your primary goal is not working code, rather to produce a great desing, which happens to work.
- strategic programming requires an investment mindset, rathe than taking the fastest path to finish your current project, you must invest time to improve the design of the system. Some investment areas:
- proactive
- finding a simple desing for each new class, rather than implementing the first idea that comes to mind. Try to imagine a few ways in which the system might need to be changed in the future adn make sure that will be easy with your design
- writing good documentation
- reactive:
- dont ignore or patch around mistakes in design decisions; take a little extra time to fix it.
- How much time to invest: ~10-20% of your development time in investments
-
Technical debt: a term used to describe the problems caused by tactical programming. By programming tactically, you're borrowing time from the future. Development will go more quickly now, but more slowly later on.
Chapter 4 - Modules should be deep
- Use modular design [1] to minimise dependencies between modules [2].
- A developer should not need to understand the implementation of modules other than the one they are working in.
- The best modules are deep [3]
- A module's interface represents the complexity it imposes on the rest of the system: the smaller and simpler the interface, the less complexity it introduces.
- A shallow module is one whose interface is relatively complex in comparison to the functionality it provides.
- The value of deep classes is not widely appreciated today. Conventional wisdom in programming is that classes should be small, not deep. Students are often taught that the most important thing in class design is to break up larger classes into smaller ones. Same advice is given about methods: "Any method longer than N lines should be divided into multiple methods". (N can be as low as 10). This approach results in large number of shallow classes and methods, which add to overall system complexity. This phenomenon is termed classitis small classes result in a verbose programming style, due to the boilerplate required for each class.
An example of a module that suffers from classitis is the java.io package, with many small classes e.g BufferedInputStream, ByteArrayInputStream, DataInputStream,... Compare this to the java.util.stream.Collectors interface which is deep.
Chapter 5 - Information Hiding (and Leakage)
- When designing a new module, you should think carefully about what information can be hidden in that module. If you can hide more information, you should also be able to simplify the module's interface.
- Information leakage occurs when a design decision is reflected in multiple modules. This creates a dependency between the modules: any change to that design decision will require changes to all of those involved modules.
- If a piece of information is reflected in the interface of the module, then has leaked.
- One way information leakage becomes obvious is where you have to use many small classes to do a small operation.
- One way to solve information leakage is to ask "How can I reorganise these classes so that this particular piece of knowledge only affects a single class?".
- One common cause of information leakage is a design style - temporal decomposition: where the structure of a system corresponds to the time order in which operations will appear. Operations that happen at different times are in different methods or classes.
wherever possible, classes should "do the right thing" without being explicity asked.
Chapter 6 - General purpose modules are deeper
Unnecessary specialization, whether in the form of special-purpose classes and methods or special cases in code, is a significant contributor to software complexity.
Specialization can't be eliminated completely, but with good design can be reduced significantly and separate specialized code from general purpose code; resulting in deeper classes, better information hiding and simper and more obvious code.
An approach for managing software complexity, such that developers only need to face a small fraction of the overall complexity at any tiven time. โฉ๏ธ
A module can take many forms: classes, subsysytems or services. โฉ๏ธ
Deep modules provide powerful functionality, yet have simple interfaces. A lot of functionality is hidden behind a simple interface. โฉ๏ธ
Member discussion