Notes of A Philosophy of Software Design--Principles
I never believed that there is a book that can fill the gap between theory and practice, especially for system design, but I do believe that the book “A Philosophy of Software Design” is the best one that narrows the gap.
Chapter 1: Introduction
The whole section (and the entire book) is all about complexity, it is exactly what I am looking for since I always appreciate simple but practical ideas.
Software 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. I have seen many programs including my own ones, which were succinct and elegant at the very beginning but became bulky and complex eventually. So, I somehow think complexity is the final and inevitable destiny of any software system, but we should learn the skills that could delay the arrival of such destiny, like the statement in the book, “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”. As illustrated in the book, there are two approaches to fighting complexity, one is to make code simpler and more obvious (e.g. eliminating special cases or using identifiers in a consistent fashion), the other is to encapsulate the system (i.e. modular design). Either the former or the latter is usually a case-by-case thing, some useful principles and tricks are proposed over the last several decades, but there is no one-size-fits-all method.
Lifecycle of Design
Software design can be developed in two fashions through the lifecycle. One method is called waterfall model, in which a project is divided into discrete phases such as requirement definition, design, coding, testing, and maintenance. The design phases usually take place at the beginning of the project and only once. However, it isn’t possible to visualize the design for a large software system and foresee all the details (or even problems) well enough before building anything. I learned this method when I was an undergraduate, and I do believe it is outdated, no (decent) company really applies it. In reality, developers always try to patch around the problems without redesigning the entire system, which results in an explosion of complexity.
Since the software is malleable, the design should be a continuous process that spans the entire lifecycle of a software system. So the mainstream is the incremental methods such as agile development, in which the project starts from a small subset of the overall functionality (or I called it the prototype that only has key functions) and grows iteratively through design, implementation, and then evaluation. The book is about how to use complexity to guide the design of software throughout its lifetime.
Chapter 2: The Nature of Complexity
Taking one step back, we have to ask ourself the following questions.
- What is the definition of “complexity”?
- How to tell if a system is unnesscessiary complex?
- What causes systems to become complex?
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.
Defintion of Complexity
The complexity is defined in a practical way, which is illustrated as follows,
Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.
Some examples of complexity can be
- hard to understand how a piece of code works
- take a lot of effort to implement a small improvement
- it might not be clear which parts of the system must be modified to make the improvement
- difficult to fix one bug without introducing another
The complexity can be defined in a cost-benefit way as well.
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.
Note: Complexity doesn’t necessarily related to the overall size or functionality of the system, but I have never seen a large and spohisticated system is simple or easy to work on or convenient to implement improvements. Anyone can make a spohisticated but simple system is a master.
Furthermore, there is an equation to describe how the activities affect the system complexity.
\[C = \sum_{p}c_p t_p\]Where $C$ is the overall complexity of a system, $c_p$ is the complexity of each part, $t_p$ is the fraction of time developers spend working on the part $p$. So, it should be fine if there are several super complex components in a system but developers rarely work on them.
Symptoms of Complexity
Complexity manifests itself in three general ways, change amplification, cognitive load, and unknown unknowns.
Change amplification: a simple change requires code modifications in many different places. It is easy to understand. Think about why we need a global or share variable sometimes in a system.
Cognitive load: how much a developer needs to know in order to complete a task. A higher cognitive load means that developers have to spend more time learning the required information, and these is a greater risk of bugs because they have missed something important. There are some interesting discussions about it actually. Some developers believe well-written document can significantly reduce the cognitive load, but the others may think we actually don’t need document at all if the code is clear enough. Furthermore, the cognitive load shouldn’t be measured by lines of code.
Unknown unknowns: 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 is the worst symptoms among all three, and there is no cure. Developers may have to read every line of code to figure out the problems.
Causes of Complexity
Complexity is caused by two things: dependencies and obscurity.
Dependency is inevitable and sometimes on purpose. When we have modular design and implementation, modules have to depend with each other. However, 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 possible.
Obscurity occurs when important information is not obvious. A classic example is a generic variable name like time
, or accuracy
. What’s worse, obscurity is often associated with dependencies, where it is not obvious that a dependency exists.
Chapter 3: Strategic vs. Tactical Programming
Tactical programming is short-sighted. It is always trying to generating “working code” as soon as possible (or before a deadline) and tolerating a small kludge or two for everytime. As a result, the complexities accumulate rapidly since every programming task will contribuion a few of them, especifically if everyone is programming tactically.
Strategic programming requires to invest time on designing the system. Although design problems are inevitable, taking a little extra time to fix them rather than just ignoring or patching around them. If you program strategically, you will continually make small improvements to the system design.
Tthe best approach is to make lots of small investments on a continual basis. Spending about 10-20% of the total development time on investments is good choice. Over time, the strategic approach results in greater progress.