Skip to content

Instantly share code, notes, and snippets.

@wataruoguchi
Created September 6, 2021 19:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wataruoguchi/c608843e6db2e107e477008a2a70ab94 to your computer and use it in GitHub Desktop.
Save wataruoguchi/c608843e6db2e107e477008a2a70ab94 to your computer and use it in GitHub Desktop.
A Philosophy of Software Design (Chapter 1 - 4) #bookclub

A Philosophy of Software Design

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. Problem decomposition is the central design task that programmers face every day, and yet, other than the work described here, I have not been able to identify a single class in any university where problem decomposition is a central topic.

Chapter 1 - Introduction (It's all about complexity)

Good development tools can help us deal with complexity, and many great tools have been created over the last several decades. But there is a limit to what we can do with tools alone. If we want to make it easier to write software, so that we can build more powerful systems more cheaply, we must find ways to make software simpler.

The first approach is to eliminate complexity by making code simpler and more obvious.

e.g., Functional Programming

The second approach to complexity is to encapsulate it, so that programmers can work on a system without being exposed to all of its complexity at once.

e.g., Object Oriented Programming

(D)evelopers try to patch around the problems without changing the overall design. This results in an explosion of complexity.

Incremental development means that software design is never done. Design happens continuously over the life of a system: developers should always be thinking about design issues.

It's like gardening.

If software developers should always be thinking about design issues, and reducing complexity is the most important element of software design, then software developers should always be thinking about complexity.

1.1 How to use this book

Reviewing code will also expose you to new design approaches and programming techniques.

Code reviewing - are you doing it well?

Chapter 2 - The Nature of Complexity

2.1 Complexity defined

Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.

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. When you find yourself in situations like this, it’s worth probing the other developers to find out why the code seems complex to them; there are probably some interesting lessons to learn from the disconnect between your opinion and theirs.

2.2 Symptoms of complexity

Change amplification: The first symptom of complexity is that a seemingly simple change requires code modifications in many different places.

Cognitive load: The second symptom of complexity is cognitive load, which refers to how much a developer needs to know in order to complete a task.

Unknown unknowns: 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.

2.3 Causes of complexity

Complexity is caused by two things: dependencies and obscurity.

one of the goals of software design is to reduce the number of dependencies and to make the dependencies that remain as simple and obvious as possible.

The need for extensive documentation is often a red flag that the design isn’t quite right. The best way to reduce obscurity is by simplifying the system design.

2.4 Complexity is incremental

The incremental nature of complexity makes it hard to control. It’s easy to convince yourself that a little bit of complexity introduced by your current change is no big deal.

In order to slow the growth of complexity, you must adopt a “zero tolerance” philosophy, as discussed in Chapter 3.

2.5 Conclusion

As complexity increases, it leads to change amplification, a high cognitive load, and unknown unknowns. As a result, it takes more code modifications to implement each new feature.

Chapter 3 - Working Code Isn't Enough (Strategic vs. Tactical Programming)

Many organizations encourage a tactical mindset, focused on getting features working as quickly as possible. However, if you want a good design, you must take a more strategic approach where you invest time to produce clean designs and fix problems.

3.1 Tactical programming

The problem with tactical programming is that it is short-sighted. If you’re programming tactically, you’re trying to finish a task as quickly as possible. Perhaps you have a hard deadline. As a result, planning for the future isn’t a priority. You don’t spend much time looking for the best design; you just want to get something working soon. You tell yourself that it’s OK to add a bit of complexity or introduce a small kludge or two, if that allows the current task to be completed more quickly.

you will tell yourself that it’s more important to get the next feature working than to go back and refactor existing code.

In some organizations, management treats tactical tornadoes as heroes. However, tactical tornadoes leave behind a wake of destruction. They are rarely considered heroes by the engineers who must work with their code in the future.

3.2 Strategic programming

The first step towards becoming a good software designer is to realize that working code isn’t enough.

so your most important job as a developer is to facilitate those future extensions.

Your primary goal must be to produce a great design, which also happens to work. This is strategic programming.

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.

3.3 How much to invest?

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.

I suggest spending about 10–20% of your total development time on investments.

3.4 Startups and investment

If you are in a company leaning in this direction, you should realize that once a code base turns to spaghetti, it is nearly impossible to fix. You will probably pay high development costs for the life of the product.

The best way to lower development costs is to hire great engineers: they don’t cost much more than mediocre engineers but have tremendously higher productivity.

Facebook is an example of a startup that encouraged tactical programming. For many years the company’s motto was “Move fast and break things.”

Over time the company realized that its culture was unsustainable. Eventually, Facebook changed its motto to “Move fast with solid infrastructure” to encourage its engineers to invest more in good design.

3.5 Conclusion

Chapter 4 - Modules Should Be Deep

4.1 Modular design

In modular design, a software system is decomposed into a collection of modules that are relatively independent. Modules can take many forms, such as classes, subsystems, or services.

In order to manage dependencies, we think of each module in two parts: an interface and an implementation.

Typically, the interface describes what the module does but not how it does it. 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.

4.2 What's in an interface?

The interface to a module contains two kinds of information: formal and informal.

the formal interface for a method is its signature, which includes the names and types of its parameters, the type of its return value, and information about exceptions thrown by the method.

The formal interface for a class consists of the signatures for all of its public methods, plus the names and types of any public variables.

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.

Side effect?

The informal aspects of an interface can only be described using comments, and the programming language cannot ensure that the description is complete or accurate1.

One of the benefits of a clearly specified interface is that it indicates exactly what developers need to know in order to use the associated module.

4.3 Abstractions

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

An abstraction can go wrong in two ways. First, it can include details that are not really important; when this happens, it makes the abstraction more complicated than necessary, which increases the cognitive load on developers using the abstraction. The second error is when an abstraction omits details that really are important. This results in obscurity: developers looking only at the abstraction will not have all the information they need to use the abstraction correctly.

4.4 Deep modules

4.5 Shallow modules

Red Flag: Shallow Module

A shallow module is one whose interface is complicated relative to the functionality it provides. Shallow modules don’t help much in the battle against complexity, because the benefit they provide (not having to learn about how they work internally) is negated by the cost of learning and using their interfaces. Small modules tend to be shallow.

4.6 Classitis

The extreme of the “classes should be small” approach is a syndrome I call classitis

4.7 Examples: Java and Unix I/O

interfaces should be designed to make the common case as simple as possible

Almost every user of file I/O will want buffering, so it should be provided by default.

4.8 Conclusion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment