Writing computer software is one of the purest creative activities in the history of the human race. Programmers aren’t Programmers bound by practical limitations such as the laws of physics; We can create exciting virtual worlds with behaviors that could never exist in the real world. Programming doesn’t require great physical skill or coordination, like ballet or basketball. All programming requires is a creative mind and the ability to organize your thoughts. If you can visualize a system, you can probably implement it in a computer program.

Writing calculator software is one of the purest creative activities in human history. Programmers are not bound by the rules of reality (such as the laws of physics), and we can create exciting virtual worlds that are detached from reality. Programming doesn’t require special physical skills or group coordination like ballet or basketball, just creative thinking and the ability to organize ideas. If you can visualize a system, you can probably program it.

This means that the greatest limitation in writing software is our ability to understand the systems we are creating. As a program evolves and acquires more features, it becomes complicated, with subtle dependencies between its components. Over time, complexity accumulates, and it becomes harder and harder for programmers to keep all of the relevant factors in their minds as they modify the system. This slows down development and leads to bugs, which slow development even more and add to its cost. Complexity increases inevitably over the life of any program. The larger the program, and the more people that work on it, the more difficult it is to manage complexity.

This means that the greatest limitation of programming is the ability to understand the system currently being created. As a program evolves and acquiesces more features, it becomes complex, maintaining subtle interdependencies with its components. As complexity builds up over time, it becomes harder and harder for programmers to keep these relevant factors in mind when the system needs to be adjusted. This slows development down and creates more bugs, which in turn slows development down and increases development costs in a vicious cycle. Complexity increases over the course of any program’s development cycle. The bigger the program, the more people it takes to develop it, and the harder it is to control the 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. 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.

Good development tools can help us control complexity, and there have been many in the last decade. But there is a limit to what you can do with these tools. If we want to make software development easy, and therefore make it easy to develop powerful systems, then we must make software development easy. The complexity is still increasing, despite our best efforts to control it. Simpler designs allow us to build bigger and more powerful systems.

There are two general approaches to fighting complexity, both of which will be discussed in this book. The first approach is to eliminate complexity by making code simpler and more obvious. For example, complexity can be reduced by eliminating special cases or using identifiers in a consistent fashion.

There are two general ways to combat complexity, which are described in the book. The first is to make the code simpler and more subtle, such as reducing special cases or using identifiers in a consistent code style.

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. This approach is called modular design. In modular design, a software system is divided up into modules, such as classes in an object-oriented language. The modules are designed to be relatively independent of each other, so that a programmer can work on one module without having to understand the details of other modules.

The second is encapsulation, which allows programmers to develop without having to see the complexity at once. This approach, called modular design, divides software systems into modules, which in object-oriented languages can be in the form of classes. Modules are designed to be relatively independent of each other, so that developing one does not require understanding the details of the other.

Because software is so malleable, software design is a continuous process that spans the entire lifecycle of a software system; this makes software design different from the design of physical systems such as buildings, ships, or bridges. However, software design has not always been viewed this way. For much of the history of programming, design was concentrated at the beginning of a project, as it is in other engineering disciplines. The extreme of this approach is called the waterfall model, in which a project is divided into discrete phases such as requirements definition, design, coding, testing, and maintenance. In the waterfall model, each phase completes before the next phase starts; in many cases different people are responsible for each phase. The entire system is designed at once, during the design phase. The design is frozen at the end of this phase, and the role of the subsequent phases is to flesh out and implement that design.

Because software is so malleable, software design is an ongoing process that spans the entire lifecycle of a software system. This distinguishes software development from the design of real-world systems, such as buildings, ships, and Bridges. Software design, however, was not conceived in this way from the beginning, and the history of programming has shown that the work of design is more concentrated in the initial stages of project development, as it is in other engineering disciplines. The extreme version of this approach, called the Waterfall flow model, divides a project into phases such as requirements review, design, development, testing, and maintenance. Under the waterfall model, one phase is completed before another, and in many cases different people are responsible for different phases. The whole system is designed in one go, the design is fixed at the end of its phase, and the next phase is to construct and flesh out the design.

Unfortunately, the waterfall model rarely works well for software. Software systems are intrinsically more complex than physical systems; it isn’t possible to visualize the design for a large software system well enough to understand all of its implications before building anything. As a result, the initial design will have many problems. The problems do not become apparent until implementation is well underway. However, the waterfall model is not structured to accommodate major design changes at this point (for example, the designers may have moved on to other projects). Thus, developers try to patch around the problems without changing the overall design. This results in an explosion of complexity.

Unfortunately, the waterfall model rarely performs well. Software systems are actually more complex than physical systems. It is impossible to fully visualize large software systems and then fully understand what they mean before they are built. As a result, there were many problems with the initial design. The problem does not arise until the requirements are fully realized. However, at this point, the waterfall flow model is not designed to accommodate large changes (e.g., the designer has been assigned to another project). As a result, developers have to find ways to tinker with problems without changing the overall design, resulting in an explosion of complexity.

Because of these issues, most software development projects today use an incremental approach such as agile development, in which the initial design focuses on a small subset of the overall functionality. This subset is designed, implemented, and then evaluated. Problems with the original design are discovered and corrected, then a few more features are designed, implemented and evaluated. Each iteration exposes problems with the existing design, which are fixed before the next set of features is designed. By spreading out the design in this way, problems with the initial design can be fixed while the system is still small; later features benefit from experience gained during the implementation of earlier features, so they have fewer problems.

For all these reasons, most software development projects today take an incremental approach, such as agile development, where the initial design focuses on only a small part of the total functionality. This set of molecules is designed, implemented, and then evaluated. Problems with the first version are identified and corrected, and more smaller subsets of functionality are then designed, implemented, and evaluated. By identifying problems with the existing design with each iteration, they can be eliminated before the next feature is designed. With this diffuse design, the first edition problems were fixed when the system was still small. By the time the later features are implemented, the previous fixes will become experience, so there will be fewer problems than in the previous phase.

The incremental approach works for software because software is malleable enough to allow significant design changes partway through implementation. In contrast, major design changes are much more challenging for physical systems: for example, it would not be practical to change the number of towers supporting a bridge in the middle of construction.

Incremental development works because the software becomes resilient enough to make major changes to the design along the way. By contrast, large design changes are difficult for physical systems. For example, it is impractical to change the number of columns that bear weight during the construction of a bridge.

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. Incremental development also means continuous redesign. The initial design for a system or component is almost never the best one; experience inevitably shows better ways to do things. 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 improvements.

Incremental development means that software design is never-ending. Design is continuous throughout the life cycle of a system: developers need to think about design issues all the time. Incremental development also means constant redesign. The first version of the component is never the best version of the system, and inevitably, experience will show you a better way to implement it. As a developer, you must always be on the lookout to improve the design at hand whenever you can, and plan to improve it whenever you have time.

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. This book is about how to use complexity to guide the design of software throughout its lifetime.

If software developers can always think about design and think that reducing complexity is the first step in software development, then complexity is something they will always think about. This book is about using complexity as a guide to where design should go in the software design lifecycle.

This book has two overall goals. The first is to describe the nature of software complexity: What does “complexity” mean, why does it matter, and how can you recognize when a program has complexity? The book’s second, and more challenging, goal is to present techniques you can use during the software development process to minimize complexity. Unfortunately, There isn’t a simple recipe that will guarantee great software designs. Instead, I will present a collection of higher-level concepts that border on the philosophical, Such as “classes should be deep” or “define errors out of existence.” These concepts may not immediately identify the best design, but you can use them to compare design alternatives and guide your exploration of the design space.

This book has two main purposes. The first is to explain the nature of software complexity: what it is, why it is key, and how to spot unnecessary complexity in a program. Second, and more difficult, are techniques that can be used to minimize complexity in the software development process. Unfortunately, there is no simple formula that can guarantee a large software design. Instead, I offer a set of higher-order ideas that are close to philosophy, such as “classes need depth” or “define problems in the absence of existence.” These ideas may not immediately identify the best design, but if you use them to compare your candidates, they can lead you to explore the vast ocean of design.