Uncle Bob’s Clean Architecture is an important reference book for software developers. The author introduces the reader into the realm of software architecture, telling that “the rules of software architecture are the rules of ordering and assembling the building blocks of programs” and that these blocks are universal and haven’t changed since he started in the profession: they’re universal and changeless.
The introductory chapters describe the goal and the importance of good software architecture. Down to its essence, the goal of software architecture is to minimize the human resources required to build and maintain the system. Uncle Bob reinforce this message reproducing a case study that shows how bad architecture decreases the developers’ productivity and raises the system’s maintenance costs.
He explains that a software system provides two different values to the stakeholders: behavior and structure. The behavior is the set of coded functional specifications (business rules, use cases) while the structure is the architecture of the system. The architecture is of greater value because when done right, support best the business because it makes the system easier to change and evolve.
The author then gives a brief overview of the three main programming paradigms (and in his words, the only available paradigms): procedural programming, object-oriented programming, and functional programming. Each of these paradigms restricts some aspect of how we write code. They tell us what not to do. We must create modules that are functionally decomposable, not using jumping instructions (goto). We must use polymorphic interfaces to invert the flow of control, enabling a plugin architecture that rightly separates the concerns of the system. We must avoid mutable variables to prevent all the sorts of concurrency issues.
And before exploring the architecture principles, he gives an overview of the SOLID design principles (which are principles suited to the groupings of function and data, like the classes of the OO languages) and the components’ principles (the rules that govern how the code must be grouped into components and how the components should couple to each other).
Then it comes the main part of the book, which shows the features of a good architecture. It starts reinforcing his definition of architecture, which “is the shape given to the system by those who build it”. The form of this shape is in the division of the system into the components, their arrangement and the ways they communicate with each other. Using the outline of the chapters that compose this part, a good software architecture:
- Must allow for independent deployability and develop-ability
- Must have clear boundaries between the components, with the dependencies pointing in one direction: toward the core business
- Can have a mixture of chatty boundaries (same process, threads) or slow boundaries (local processes, services) between the components
- Have the policies grouped at the right level: lower-level policies change frequently while higher-level policies are more stable. The source code dependencies must always point to the high-level policies
- Have pristine business rules, located at the heart of the software
- Must not be based on frameworks. The architecture must tell its users (the developers) what is the theme of the system
- Must be independent of frameworks, of the UI, of the database and from the interfaces to the outside world (i.e. HTTP, network protocols)
- Must be testable
It is important to notice that this is a book that is supported by ideas. The author uses code examples sparingly and supports his idea through the heavy use of simple diagrams. I see this as an odd decision, one that can bring too much confusion to less experienced software developers. Books like Growing Object-Oriented Software (Nat Pryce and Steve Freeman) and Implementing Domain-Driven Design (Vaughn Vernon) support better their points when discussing architectural options with good code examples.
Besides that, the aforementioned books are a great and supplemental reading on the topic of software architecture. Uncle Bob’s Clean Architecture will give food for thought for architectural discussions while the other two books will give more approachable examples of the architectural concerns.
The rules of software architecture are the rules of ordering and assembling the building blocks of programs. And since those building blocks are universal and haven’t changed, the rules for ordering them are likewise universal and changeless.
The goal of software architecture is to minimize the human resources required to build and maintain the required system.
The measure of design quality is simply the measure of the effort required to meet the needs of the customer. If that effort is low, and stays low throughout the lifetime of the system, the design is good. If that effort grows with each new release, the design is bad. It’s as simple as that.
Paradigms are ways of programming, relatively unrelated to languages. A paradigm tells you which programming structures to use, and when to use them. To date, there have been three such paradigms. For reasons we shall discuss later, there are unlikely to be any others.
Structured programming imposes discipline on direct transfer of control.
Object-oriented programming imposes discipline on indirect transfer of control.
Functional programming imposes discipline upon assignment.
OO is the ability, through the use of polymorphism, to gain absolute control over every source code dependency in the system. It allows the architect to create a plugin architecture, in which modules that contain high-level policies are independent of modules that contain low-level details. The low-level details are relegated to plugin modules that can be deployed and developed independently from the modules that contain high-level policies.
The SOLID principles tell us how to arrange our functions and data structures into classes, and how those classes should be interconnected. The use of the word “class” does not imply that these principles are applicable only to object-oriented software. A class is simply a coupled grouping of functions and data. Every software system has such groupings, whether they are called classes or not. The SOLID principles apply to those groupings.
Of all the SOLID principles, the Single Responsibility Principle (SRP) might be the least well understood. That’s likely because it has a particularly inappropriate name. It is too easy for programmers to hear the name and then assume that it means that every module should do just one thing.
The SRP says to separate the code that different actors depend on.
The OCP is one of the driving forces behind the architecture of systems. The goal is to make the system easy to extend without incurring a high impact of change.
The LSP can, and should, be extended to the level of architecture. A simple violation of substitutability, can cause a system’s architecture to be polluted with a significant amount of extra mechanisms.
(On the ISP) In general, it is harmful to depend on modules that contain more than you need. This is obviously true for source code dependencies that can force unnecessary recompilation and redeployment—but it is also true at a much higher, architectural level.
The Dependency Inversion Principle (DIP) tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.
One of the overriding concerns with this dependency structure is the isolation of volatility. We don’t want components that change frequently and for capricious reasons to affect components that otherwise ought to be stable.
If we tried to design the component dependency structure before we designed any classes, we would likely fail rather badly. We would not know much about common closure, we would be unaware of any reusable elements, and we would almost certainly create components that produced dependency cycles. Thus the component dependency structure grows and evolves with the logical design of the system.
Thus, if a component is to be stable, it should consist of interfaces and abstract classes so that it can be extended. Stable components that are extensible are flexible and do not overly constrain the architecture. The SAP and the SDP combined amount to the DIP for components. This is true because the SDP says that dependencies should run in the direction of stability, and the SAP says that stability implies abstraction. Thus dependencies run in the direction of abstraction.
The primary purpose of architecture is to support the life cycle of the system. Good architecture makes the system easy to understand, easy to develop, easy to maintain, and easy to deploy. The ultimate goal is to minimize the lifetime cost of the system and to maximize programmer productivity.
Perhaps a better way to say this is that the architecture of a system makes the operation of the system readily apparent to the developers. Architecture should reveal operation. The architecture of the system should elevate the use cases, the features, and the required behaviors of the system to first-class entities that are visible landmarks for the developers. This simplifies the understanding of the system and, therefore, greatly aids in development and maintenance.
If you can develop the high-level policy without committing to the details that surround it, you can delay and defer decisions about those details for a long time. And the longer you wait to make those decisions, the more information you have with which to make them properly.
What changes for different reasons? There are some obvious things. User interfaces change for reasons that have nothing to do with business rules. Use cases have elements of both. Clearly, then, a good architect will want to separate the UI portions of a use case from the business rule portions in such a way that they can be changed independently of each other, while keeping those use cases visible and clear.
Business rules themselves may be closely tied to the application, or they may be more general. For example, the validation of input fields is a business rule that is closely tied to the application itself. In contrast, the calculation of interest on an account and the counting of inventory are business rules that are more closely associated with the domain. These two different kinds of rules will change at different rates, and for different reasons—so they should be separated so that they can be independently changed.
So long as the layers and use cases are decoupled, the architecture of the system will support the organization of the teams, irrespective of whether they are organized as feature teams, component teams, layer teams, or some other variation.
But there are different kinds of duplication. There is true duplication, in which every change to one instance necessitates the same change to every duplicate of that instance. Then there is false or accidental duplication. If two apparently duplicated sections of code evolve along different paths—if they change at different rates, and for different reasons—then they are not true duplicates. Return to them in a few years, and you’ll find that they are very different from each other.
A good architecture will allow a system to be born as a monolith, deployed in a single file, but then to grow into a set of independently deployable units, and then all the way to independent services and/or micro-services. Later, as things change, it should allow for reversing that progression and sliding all the way back down into a monolith.
Strictly speaking, business rules are rules or procedures that make or save the business money. Very strictly speaking, these rules would make or save the business money, irrespective of whether they were implemented on a computer. They would make or save money even if they were executed manually.
The critical rules and critical data are inextricably bound, so they are a good candidate for an object. We’ll call this kind of object an Entity.
A use case is a description of the way that an automated system is used. It specifies the input to be provided by the user, the output to be returned to the user, and the processing steps involved in producing that output. A use case describes application-specific business rules as opposed to the Critical Business Rules within the Entities.
Ideally, the code that represents the business rules should be the heart of the system, with lesser concerns being plugged in to them. The business rules should be the most independent and reusable code in the system.
If your system architecture is all about the use cases, and if you have kept your frameworks at arm’s length, then you should be able to unit-test all those use cases without any of the frameworks in place. You shouldn’t need the web server running to run your tests.
Typically the data that crosses the boundaries consists of simple data structures. You can use basic structs or simple data transfer objects if you like. Or the data can simply be arguments in function calls. Or you can pack it into a hashmap, or construct it into an object. The important thing is that isolated, simple data structures are passed across the boundaries. We don’t want to cheat and pass Entity objects or database rows. We don’t want the data structures to have any kind of dependency that violates the Dependency Rule.
The Humble Object pattern is a design pattern that was originally identified as a way to help unit testers to separate behaviors that are hard to test from behaviors that are easy to test. The idea is very simple: Split the behaviors into two modules or classes. One of those modules is humble; it contains all the hard-to-test behaviors stripped down to their barest essence. The other module contains all the testable behaviors that were stripped out of the humble object.
As useful as services are to the scalability and develop-ability of a system, they are not, in and of themselves, architecturally significant elements. The architecture of a system is defined by the boundaries drawn within that system, and by the dependencies that cross those boundaries. That architecture is not defined by the physical mechanisms by which elements communicate and execute.
You can think of well-defined components in a monolithic application as being a stepping stone to a micro-services architecture.
Want to discuss this book? Reach me at Twitter!