Uncle Bob outlines what he has learned over the years when it comes to architecture on a systems and project level. The goal of architecture is to reduce the long-term cost of a project by delaying decisions and maintaining as much flexibility as possible. A special team does not accomplish this goal but architects who are the best programmers. Through the pragmatic application of principles and architectural methods, one can make a project easier and more cost effective.
- The architecture rules are the same for all different kinds of apps.
- Code is still just an assemblage of sequence, selection, and iteration. That is the reason why architecture rules stay the same.
Chapter 1: What is Architecture?
- Architecture is at all levels since the architecture and design of low-level components build up to high-level components.
- The goal of software architecture is to minimize the developer resources required to build and maintain a system.
- The race is not to the swift, nor the battle to the strong.
- Foolishness of overconfidence
- The more haste the less speed
- Making messes is always slower than staying clean
- The only way to go fast is to go well
- The same overconfidence and haste that got them into the mess is what drives them to believe that a replatform or starting over will fix their issues. It will result in another mess.
Chapter 2: A Tale of Two Values
- Software systems provide 2 different values to stakeholders:
- Software must be soft (easy to change). i.e. difficulty must be proportional only to the scope and not the shape of the change.
- The difference between scope and shape often drives growth in software development costs
Architecture should be as shape agnostic as is practical
Architecture Vs. Behaviour
- Business managers say it’s more important for the system to work but it’s the wrong attitude [most of the time]
- If it works but is impossible to change then it will become useless
- If it doesn’t work but is easy to change it will remain useful
- Eisenhower priority matrix: Behaviour urgent but not always important; Architecture is important but never particularly urgent.
- Business managers are not equipped to evaluate the importance of architecture. It is the responsibility of the software dev to assert the value of architecture over behaviour.
- If architecture comes last, the system will become ever more costly to develop.
Chapter 3: Paradigm Overview
- Paradigms are ways of programming relatively unrelated to languages
- Three paradigms emerged between 1958 and 1968: structured, OO, & functional
- Structured imposes discipline on direct transfer of control (e.g. GOTOs)
- OO imposes discipline of indirect transfer of control
- Functional imposes discipline upon assignment (no assignment of vars)
- These paradigms remove capabilities in that they impose discipline negative in its intent
- They tell us what not to do
Chapter 4: Structured Programming
- All programs can be constructed from three structures: sequence, selection, & iteration.
- Those three are what make a module proveable and are the minimum set from which all can be built.
- Those three are what make a module proveable and are the minimum set from which all can be built.
- Structured programming allows modules to be recursively decomposed into proveable units.
- Use tests to prove small proveable functions incorrect.
- Functional decomposition is a good practice
Chapter 5: Object Oriented
- The basis of a good architecture is the understanding and application of OOD
- OO is the use of polymorphism to gain control over every source code dependency in the system
- OO imposes discipline on indirect transfer of control (polymorphism)
- Any dependency can be inverted
- Gives you independent deployability
Chapter 6: Functional
- Variables in functional languages do not vary
- All race conditions, deadlocks, and concurrency problems are due to mutable variables
- All the problems that we face cannot happen if there are no mutable variables
- A common compromise is to segregate the apps into mutable and immutable components
- It is a common practice to use transactional memory to protect mutable variables from concurrent updates and race conditions
Part III: Design Principles
- Solid principles tell us how to arrange functions and data structures into classes and how those classes should be interconnected
The goal of principles is the creation of mid-level software structures that:
- Tolerate change
- Are easy to understand
- Are the basis of components in many software systems
It is just as possible to create a system-wide mess with well designed mid-level components
Chapter 7: Single Responsibility Principle
- The SRP does not mean that every module should do only one thing (though that is a worthy goal).
- A module should be responsibile to one, and only one, actor
- Cohesion is the force that binds together the code responsible to a single actor.
- Separate code that different actors deped on.
- Keep important business rules closer to the data.
Chapter 8: The Open Closed Principle
- Open for extension but closed for modification. A software artifact should be extendable without having to modify it.
- Organize dependencies to ensure that changes to a responsibility do not cause changes in ther other.
- Accomplish by partitioning into classes and separating these classes into components
- All component relations are unidirectional
- A heirarchy of protection and levels
- OCP at the architectural level: separate functionality based on how, why, and when it changes
- Invert the dependencies
- Transitive dependencies are not a violation of the principle that entities should not depend on things they don’t directly use.
- Goal: easy to change without incurring a high impact of change.
Chapter 9: Liskov Substitution Principle (LSP)
The LSP in spirit says that objects should be substitutable for their base type.
A famous example of this is the square & rectangle problem. A square is a rectangle with the same sides that must change together. Consequently, a square is not a proper subtype of a rectangle.
But the LSP pertains not just to interfaces and implementations but also to architecture. Similarly, REST services must conform to the principle.
Violations of substitutability can cause a system’s architecture to become polluted and unmaintainable.
No programmer worth their salt would use a conditional (if) to check for specifics about a type.
Chapter 10: Interface Segregation Principle
Whenever possible, it is best to not depend on modules you don’t need.
Use and declaration statements in source files are what create dependencies that force recompilation and deployment. Dynamically typed languages create systems that are, in general, more flexible and less tightly coupled.
Chapter 11: Dependency Inversion Principle (DIP)
The DIP basically states that high-level modules should not depend on low-level modules and both should depend upon abstractions.
However, in true Pirates of the Carribean fashion, it is a guideline and not a rule since we often depend on a languages standard library and on operating system utilities. These tend to be very stable. So we can see an extension of the rule is that stable architecting avoid depending on volatile concretions.
In addition, it is best not to derive from volatile concrete classes.
When you need to create a volatile concrete object us an abstract factory. DIP violations cannot be removed entirely. With this principle, dependencies are inverted against the flow of control. This rule also applies to dynamically typed languages.
Component Principle IV
- Component principles tell us how to organize rooms into buildings
Chapter 12: Components
Components are the units of deployment, the smallest entities that can be deployed as part of a system.
Murphy’s law of program size: “programs grow to fill all available compile and link time.” Goes over the history of components.
Chapter 13: Component Cohesion
REP: Reuse/Release Equivalance principle
- The granule of reuse is the granule of release
Components/modules must be tracked through a release process and given version numbers
Classes and modules in a component must form a cohesive group, a theme or purpose. And they should be releasable together.
Violations are easy to detect since they don’t make sense.
Three principles of component cohesion are fighting one another. Early on the CCP is more important than the REP. The appropriate spot in the tension triangle depends on the current state of the project.
The REP and the CCP tend to make components larger while CRP tends to make them smaller.
- REP: Reuse/release equivalency principle
- CCP: component closure principle
- CRP: common reuse principle
REP - the granule of reuse is the granule of release. Must be a cohesive group. Should be releasable together. Violations simply don’t make sense.
CCP - group classes that change for the same reasons and same times. Maintainability more important than reusability. Associated with OCP and SRP. 100% closer is not attainable.
CRP - classes and modules that tend to be reused together belong in the same component. Want to make sure to depend on every class in the component. Don’t depend on things you don’t need. Generic version of the ISP.
Chapter 14: Acyclic Dependencies Principle (ADP)
States that there should be no cycles in the component dependency graph. Too often the work of devs is dependent on the work of other devs. When it comes time to integrate their work it is a mess. The build is often broken. One solution is the weekly build but this runs into problems as the project grows since it starts taking longer and longer to integrate and deploy.
Another solution is to eliminate dependency cycles altogether by partitioning the environment into releasable components. Coupled with versioning this is a very workable scheme since integration happens in small in small increments. No single point in time.
In order for incremental deployments to work there must be no cycles or cyclic dependencies in the code - it must be a directed graph. To release the whole system, proceed from the bottom up.
Breaking a cycle - to break a cycle, either apply the DIP or create a new component.
Top down design - a component structure cannot be designed from the top down. Dependency diagrams have very little to do with describing function. They describe the buildability and maintainability. The dependency structure grows with the design of the system.
Depend in the direction of stability. Stability is related to the amount of work required to make a change.
Stable components should not depend on volatile components. Components that are depended on by lots of other components are difficult to change.
- Instability = fan-out / (fan-in + fan-out)
- Fan-in = incoming dependencies
- fan-out = outgoing dependencies
Lack of dependents gives a component no reason not to change. Not all components should be stable. Changeable (volatile) components on tob and stable components on the bottom.
Abstract components - in statically typed languages it is a useful practice to create components that contain nothing but interfaces. Those components are very stable.
Stable Abstractions Principle (SAP) - A component should be as abstract as it is stable. Interfaces and abstract classes. High-level policies should be placed into stable components. Unstable components should be concrete.
SAP + SDP = DIP for components
Dependencies run in the direction of abstraction. A component can be partially abstract and partially stable.
Measuring abstraction - ratio of abstractions to concretions in the component.
* NC: number of classes * NA: number of abstractions * A = NA / NC
Abstraction graph - Main sequence is the place to be. 0,0+1,1 are not the places to be since it’s rather painful or useless. 0,0 is fine for non-volatile components such as the SPL or OS. 1,1 is abstract with no dependents. Volatile components should be kept on the main sequence.
Distance from main sequence: D = |A+I-1|
Where I = stability + A = abstractions D = Distance
Chapter 15: What is architecture
The job of the Architect is to maximize productivity and leave as many options open for as long as possible and minimize the lifetime cost of the system. They make it so that the system is easy to understand, develop, maintain, and deploy.
However, they don’t just have their heads in the clouds - they’re programmers, the best. They can’t do their job properly if they’re not experiencing the problems they’re creating. The harder to develop the shorter the life expectancy. The higher the cost of deployment, the less useful the system is. Should be deployable with a single action.
Architectures that impede dev or deployment are more costly than those that impede operations, maintenance.
Two types of value: behaviour and structure.
Software systems contain two major components: policy and details.
Policy is the most important. Details are irrelevant to the policy. Decisions about details should be deferred. A good architect maximizes the decisions not made. High-levl policy should be against the interface to the outside world. Should not care about dependency resolution.
The longer you wait the more info you have. Good architects carefully separate details from policy and then decouple policy from the details.
Chapter 16: Independence
Any organization that design a system will produce a design whose structure is a copy of the organization’s communications structure. The decoupling made of a system is likely to change with time. If the architecture is right there it can progress from a monolith to independently deployable units to micro-services.
But one typically shouldn’t start out with microservices since they are expensive and a waste of effort. Push the decoupling to the point where a service could be formed. Three decoupling modes: source, deployment, and service level.
Resist compulsive elimination of duplication. Things that change at different rates and for different reasons are not true duplicates.
Good architecture leaves options open. Decoupling mode is one of those. Use the SRP and the CCP to separate things that change for different reasons. If you do that then it will be easy to add new use cases without interferring with old ones.
Use cases can be used as natural dividers of a system. Keep them separate down the vertical height of the system. There are few behavioural options that a system can leave open.
Chapter 17: Boundaries
Drawing lines is the art of drawing boundaries. Boundaries define a system. The SRP tells us where to put the boundaries. First you partition the system into components and then you arrange those components according to the DIP and the SAP. Doing so ensures that dependencies flow or point from the lower-level to the higher-level abstractions.
The GUI doesn’t matter to the business rules. The DB is a tool that the business rules can use indirectly. Drawing boundaries helps delay and defer decisions. Try to defer decisions into non-existence. Be weary of flushing hours down the SOA vortex needlessly. Early boundaries drawn to defer decision. Coupling saps the line and energy of the development team especially coupling to premature decisions. Making premature decisions multiplies dev efforts.
Chapter 18: Boundary Anatomy
The architecture of a system is defined by components and boudaries. There are several boundary strategies including the monolith, deployment components, local processes, and services.
Boundaries are about safeguarding against change. The monolith is the simplest form even though the boundaries are not visible during deployment. Without OO you’d have to dangerously rely upon function pointers to acheive decoupling dynamic polymorphism used to invert dependencies against the flow of control. Dependencies cross boundaries toward the higher level components. Communication fast though chatty across decoupled boundaries.
A system broken up into deployable components is essentially the same as a monolith except for being broken up and deployed using multiple files. Communications are still very chatty. A stronger physical boundary is the local process. Segregation strategy is the same. Communications involve the OS and other systems and should be limited.
The strongest bounder is a service which can be local or remote. Assume communications take place over a network and slow. Avoid chatting where possible.
Chapter 19: Policy & Level
Policies determine how a program’s inputs are transformed into outputs. Programs are essentially just statements of policy. A major part of architecture is grouping policies based on how they change (components) ensuring that low-level components depend on high-level components. Components should often form a directed acyclic graph. Code dependencies should be decoupled from data flow and coupled to level. A “level” is the distance from the inputs and the outputs. Farther from in/out = higher level.
Separating policies and ensuring that they are at the right level reduces the impact of change. Lower-level policies tend to change frequently but for trivial reasons. Policy theory touches several such principles: SRP, OCP, CCP, DIP, SDP, SAP.
Chapter 20: Business Rules
Business rules are rules or procedures that make or save the business money. They are made up of critical business rules and critical busiess data. Both would exist without the system being automated.
An Entity embodies critical business rules operating on critical business data which it contains or has easy access to. An entity is pure business. A use case defines and constrains a system in a way that would not be done in a normal environ. They define an automated system. Don’t describe how the system appears to the user. Is an object. Entities have no knowledge of the use cases that control them. Entities are higher level and use cases lower level. A use case is an object. They do not describe how the system appears. Use cases accept simple request data structures or input and returns a simple response data structure as output. These are not the framework response and request interfaces.
Do not allow these data structures to contain references to entity objects. Avoid trying them in any way to anything that violates the CCP and the SRP.
Chapter 21: Screaming Architecture
Your architecture should tell others about the system, not about the framework you’re using. Frameworks are tools to be used not architectures to be comformed to. Good architectures are centered on use cases. They should be able to be unit tested without the framework. Entity objects should be plain objects. Your architecture should be as ignorant as possible about how it is delivered. Same with frameworks and databases. Those decisions should be deferred. What does our architecture scream?
Chapter 22: The Dependency Rule
There are several different architectures, hexogonal, DCI, BCE, and Uncle Bob’s Clean Architecture. But they all acheive the same objective: separation of concerns by dividing a system into layers. They tend to have the following characteristics:
- Independent of frameworks
- Independent of the UI
- Independent of the database
- independent of any external agency
Dependency rule: source code dependencies must point only inward toward higher-level policies.
Frameworks + drivers -> interface adapters -> use cases -> entities
You may need more than 4 layers depending on your system.
Entities encapsulate enterprise-wide critical business rules.
Use cases are application specific business rules. Changes in this layer should not efect the entities.
Interface adapters convert data from the most convenient for use cases and entities to the format most convenient for some external agency. SQL restricted to this layer.
Frameworks and drivers is the outermost layer. Generally you don’t write much code in this layer.
Resolve contradictions by using the DIP. No name in an outer circle can be mentioned in an inner circle. Data that crosses boundaries should be DTOs and or structs but not entities or database rows. By separating a system into layers, you make a system that is testable.