Recently, I was reading “A philosophy of software design”, by John Ousterhout.
Nature of software complexity
Software complexity refers to any aspect of a system’s structure that makes it difficult to understand and modify.
Complexity is determined by the activities that are most common. If some parts of a system are very complicated but are almost never touched, they have little impact on the system’s overall complexity. Therefore, isolating complexity in a place where it will rarely be seen is almost as effective as eliminating it entirely.
It is also important to remember that complexity is more apparent to readers than to writers. If a piece of code seems simple to its author but complex to other developers, then it is complex.
Symptoms of Complexity
Change Amplification
A system suffers from change amplification when a simple change requires modifications in many different places.
Cognitive Load
Cognitive load refers to how much a developer needs to know in order to complete a task. Some abstractions increase complexity because they require extensive background knowledge to make even small changes.
In some cases, an approach that requires more lines of code is actually simpler because it reduces cognitive load.
Unknown Unknowns
Unknown unknowns arise when it is not obvious which pieces of code must be modified to complete a task, or what information a developer needs. This is the most severe manifestation of complexity, as it often means that required changes are only discovered after a bug appears.
This situation sometimes depends on subtle design decisions that were never documented.
One of the most important goals of good design is to make a system obvious.
Causes of Complexity
Dependencies
A dependency exists when a piece of code cannot be understood or modified in isolation. One of the main goals of software design is to reduce the number of dependencies and to make the remaining ones as simple and obvious as possible.
Obscurity
Obscurity occurs when important information is not clear or visible. Common causes include:
Important information that is not obvious, such as overly generic variable names (for example,
time).Inconsistencies, such as using the same variable name for different purposes.
Inadequate documentation.
Design Issues: When a system is clean and obvious, it requires less documentation. Poor design increases obscurity and, as a result, complexity.
Complexity Is Incremental
Complexity grows over time through the accumulation of many dependencies and obscurities, which makes it increasingly difficult to control. To slow this growth, teams must adopt a zero-tolerance philosophy toward unnecessary complexity.
Modules should be deep
Modular design is design the systems so that developers only need to face a small fraction of the overall complexity at any given time.
Modular design
Software systems are decomposed into a collection of modules that are relatively independent. Modules must work together by calling each others’s functions or methods, which create dependencies between the modules.
To identify the dependencies we think in the modules as a interface and a implementation. The interface is everything that a developer must know to use the module. The implementation is the code that carries the promises made by the interface.
The best modules are those whose interfaces are much simpler than their implementations.
The interface to a module contains two kinds of information: formal and informal. The formal interface for a method is its signature, and the informal is the rest of the information that a developer needs to know to use a module, which are usually just described using comments.
Abstractions
An abstraction is a simplified view of an entity, which omits unimportant details.
Each module provides an abstraction. The interface presents a simplified view of the module’s functionality. An abstraction can go wrong in two ways:
It can include details that are not really important, which make the abstraction more complicated that necessary.
It can omit details that are really important, this result in obscurity.
So, the key to design abstractions is to understand what it’s important.
Deep modules
The best modules are those that provide powerful functionality yet have simple interfaces. A deep module is a good abstraction because only a small fraction of its internal complexity is visible to its users.
Shallow modules
A shallow module is one whose interface is relatively complex in comparison to the functionality that it provides, it is no simpler to think about the interface than to think about the full implementation.
Classitis
Usually, it’s recommended that the classes and the methods should be small, the problem of this approach is that this generate a large number of shallow classes, they are individually simple, but they add a lot of complexity to the system, and also result in a more verbose programming style.
Interfaces should be designed to make the common case as simple as possible.
Information hiding (and leakage)
Information hiding
One of the most important techniques for achieving deep modules is information hiding. The idea is that each module should encapsulate a few pieces of knowledge, which represents design decisions. The knowledge is embedded in the module’s implementation but doesn’t appear in its interface, so it is not visible to other modules. It usually consists of details about how to implement some mechanism.
The hidden information includes data structures and algorithms related to the mechanism. It can also include lower-level details such as the size of a page, and it can include higher-level concepts that are more abstract, such as an assumption that most files are small.
It simplifies the interface of a module and reduces the cognitive load on developers who use the module.
Information leakage
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 the involved modules.
Information leakage is one of the most important red flags in software design.
Temporal decomposition
In temporal decomposition, the structure of a system corresponds to the time order in which operations will occur.
When designing modules, focus on the knowledge that’s needed to perform each tasks, not the order in which tasks occur.
Information hiding can often be improved by making a class slightly larger.
Default values of parameters 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.
General-Purpose modules are deeper
Over-specialization usually introduces complexity without providing significant additional functionality, and it is more of a tactical investment.
One of the best ways to produce a deep API is to make it general-purpose. This allows it to address a broad range of problems and typically results in better information hiding.
One of the most effective ways to simplify code is to eliminate special cases, so that the common-case code also handles edge cases.
When creating a general-purpose solution, it may include facilities that are never actually needed. The sweet spot is to implement new modules in a somewhat general-purpose manner. A module’s functionality should reflect current needs, but its interface should not. The interface should be general enough to support multiple use cases.
Over-specialization cannot be completely eliminated, but it should be pushed upward and downward, into the application and infrastructure layers.
One of the most important aspects of software design is determining who needs to know what, and when. When details are important, it is better to make them explicit and as obvious as possible.
Different Layer, Different Abstraction
Software system are composed of layer, where higher layers use the facilities provided by lower layers. Each layer provides a different abstraction from the layers above and below it. If adjacent layers have similar abstractions, this is a red flag that suggest a problem with the class decomposition.
Pass-through methods
A pass-through method is one that just invoke another method with the same API. This indicates a not clean division of responsibilities and make the classes shallower. Introduce complexity but don’t add functionality to the system. The solution is to refactor the classes so that each class has a distinct and coherent set of responsabilities.
When is interface duplication OK?
Having methods with the same signature is not always bad. The imporant thing is that each new method should contribute significant functionality. One example is a dispatcher. A dispatcher is a method that uses its arguments to select one of several other methods to invoke.
Decorators
The decorator design pattern encourages API duplication across layers. The motivation is separate special-purpose extensions of a class from a generic core. Decorators tend to be shallow. Before creating a decorator consider the following alternatives:
Add the new functionality directly to the underlying class.
Merge it with the use case.
Merge with an already existing decorator.
Could be implemented as stand-alone?
Is it a wrapper to translated a external class whose interface cannot be modified, but it must conform a different interface?
Interface versus implementation
The interface of a class should normally be different from its implementation: the representations used internally should be different from the abstractions that appear in the interface.
Pass-through variables
Another form of API duplication across layers is a pass-through variable, which is a variable that is passed down through a long chain of methods. They add complexity because they force all of the intermediate methods to be aware of their existence, even though the methods have no use for the variables.
One way of solve this is introduce a context object which stores all of the application’s global state. It will probably be needed in many places, so it can potentially become a pass-through variable. Without discipline, a context can turn into a huge grab-bag of data that creates non obvious dependencies throughout the system. They may also have thread-safe issues, so the best option is that variables of a context should be inmutable.
Pull complexity downwards
Most modules have more users than developers, so it is better for the developers to suffer than the users, what means than it is more important for a module to have a simple interface than a simple implementation.
Configuration parameters provide an easy excuse to avoid dealing with important issues and pass them on to someone else. When you do create configuration parameters, see if you can provide reasonable defaults, so users will only need to provide values under exceptional conditions.
Taking it too far
An extreme approach would be to pull all of the functionality of the entire application down into a single class, what clearly doesn’t make sense. You should pull down complexity if:
It is closely related to the class’s existing functionality.
It will result in simplification elsewhere in the application.
Simplifies the class’s interface.


