Book Overview – Clean Architecture by Robert C. Martin
Introduction
If architecture comes last:
The only way to go fast, is to go well. (p. 11)
Value of software:
- Behavior
- Easily changeable (it is called “soft”-ware) = architecture!
Elsenhower’s Matrix (see below)
- Architecture = 1, 2
- Behavior = 1, 3
- Issue is that we sometimes make #3 items equal #1 items (urgent means important)
1 Important Urgent | 2 Important Not Urgent |
3 Unimportant Urgent | 4 Unimportant Not Urgent |
Paradigms:
- Structured programming
- Object oriented – imposes discipline on indirect transfer of control
- Functional – imposes discipline upon assignment
Paradigms tell us what not to do more than what to do.
Dependency Inversion (p. 44)
- develop to interface
- UI, business rules, database are independent
Independent Developability
- different teams working on parts of a system
- deploy independently
Functional programming is important to consider mutable variables.
Wise to push as much processing into immutable components (p. 54)
Event Sourcing
- Storing transactions, but not state
- needs short cuts as system grows
Goals of architecture:
- tolerate change
- easy to understand
- components use in many software systems
SOLID Principles
- SRP – Single Responsibility Principle
- OCP – Open-Closed Principle
- LSP – Liskov Substitution Principle
- ISP – Interface Segregation Principle
- DIP – Dependency Inversion Principle
SRP – Single Responsibility Principle
- A module should have one, and only one, reason to change.
- A module should be responsible to one, and only one, user or stakeholder.
- A module should be responsible to one, and only one, actor. (final version, p. 62)
- Module = cohesive set of functions and data structures
- Violations:
- Shared methods (trying to have multiple actor functions in same module)
- Merges = shared resources by different actors risks both when changing the shared code
- Conclusion: The SRP is about functions and classes – but it reappears in a different form at two more levels. At the level of components it becomes the Common Closure Principles. At the architectural level, it becomes the Axis of Change responsible for the creation of Architectural Boundaries.
OCP – Open-Closed Principle
- A software artifact should be open for extension but closed for modification. (p. 70)
- Example: displaying the same data different ways (web/print)
- Communication between modules is unidirectional. One knows about the other, but not respectively.
- Create a hierarchy of protection based on the notion of level (p. 74)
- Business logic = highest
- Views = lowest
- Architects separate functionality base on how, why, and when it changes, and then organizes that separated functionality into hierarchy of components. Higher level components protected from the changes of lower level components.
- Also aids in Directional control and Information Hiding between components
- Conclusion: 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. This goal is accomplished by partitioning the system into components, and arranging those components into a dependency hierarchy that protects higher-level components from changes in lower-level components.
LSP – Listkov Substitution Principle
- Write to an interface, not implementations of an interface.
- Conclusion: 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.
ISP – Interface Segregation Principle
- Do not depend on modules you do not need.
- Depending on modules you don’t need brings the baggage of those modules into your application.
- Related to the Common Reuse Principle
DIP – Dependency Inversion Principle
- The most flexible systems are those in which source code dependencies refer only to abstractions, not concretions.
- Stable Abstractions
- Interfaces change less frequently than the concrete implementations
- Don’t refer to concrete classes, refer to interfaces instead
- Don’t derive from concrete classes
- Don’t override concrete functions
- Abstract Factories are used if concrete objects are needed
Component Principles
If the SOLID principles tell us how to arrange the bricks into walls and rooms, then the compnent principles tell us how to arrange the rooms into buildings. Large software systems, like large buildings, are built out of smaller components.
REP – Reuse/Release Equivalence Principle
- The granule of reuse is the granule of release
- Classes and modules grouped together into a component are related together
- They share the same release version number
CCP – Common Closure Principle
- Gather into components those classes that change for the same reasons and at the same times. Separate into different components those classes that change at different times and for different reasons.
- Component form of SRP
- For the most part, maintainability is more important than reusablility.
- Components are filled with the code that will change together.
- Closely associated with the Open Closed Principle (OCP), but 100% closure is not attainable so we have to be strategic.
- we design our classes such that they are closed to the most common kinds of chnages we expect or have experienced, and when a change in requirements comes along, that change has a good chance of being restricted to a minimal number of components.
CRP – Common Reuse Principle
- Don’t force users of a component to depend on things they don’t need.
- Classes and modules that tend to be reused by each other should go in the same component
- Classes not tightly bound to each other should not be in the same component
Component Coupling
Dealing with the relationships between components.
ADP – Acyclic Dependencies Principle
- Allow no cycles in the component dependency graph.
- Each team responsible for their components. As components release, other teams chose when to upgrade.
- DAG – Directed Acyclic Graph (no cycles)
- Regardless of which component you begin at (when looking at a dependency diagram) it is impossible to follow the dependency relationships and wind up back at that component
- A structure that has no cycles
- If there are cycles in a dependency graph, two primary mechanisms for breaking the cycle:
- Apply the Dependency Inversion Principle (DIP)
- Create a new component that entities in a cycle depend on
- Component dependency diagrams have little to do with the function of an application and are instead a map of builability and maintainability (p. 119)
- DAG protects stable, high-value components from volatile components
SDP – Stable Dependencies Principle
- Depend in the direction of stability
- Stability = hard to change
- When many components rely on a single component, that component is considered stable
- When a component doesn’t depend on any other component, we say that component is independent
- Not all components should be stable – if all the components in a system were maximally stable, the system would be unchangeable. In a component diagram place all the unstable components at the top and then move down to the most stable components at the bottom. This is useful because any arrow that points up is violating the SDP (as well as ADP.)
SAP – Stable Abstractions Principle
A component should be as abstract as it is stable (p. 126)
- Relationship ebtween stability and abstractness
- stables components should also be abstract so that its stability does not prevent extension
- also… unstable component should be concrete since its instability allows the concrete code within it to be easily changed
- The SAP and the SDP combined amount to the DIP for components. (p. 127)
- DIP is a principle regarding classes – which can only be abstract or concrete. SDP/SAP is about components which is a blend of abstract and concrete classes.
- A ratio of abstract and concrete classes in a component measures its “abstractness”
- We can graph components as a relationship between stability (I) and abstractness (A) [see chart below]
- Main Sequence = this is a health mix of stability and abstractness for a component
- Zone of Pain = (0,0) highly stable (depended on) and highly concrete. Cannot be extended because it is not abstract, but cannot change because of all the dependencies. (think of system String class)
- Zone of Uselessness = (1,1) totally abstract has no dependents. Just useless, needs to be removed. Typically leftovers from development past.
- It is useful to track a system over time to see if components begin to move away from “the main sequence”. If so, a refactor might be in order.
What is architecture?
- The architecture of a software system is the shape given to that system by those who build it. The purpose of that shape is to facilitate the development, deployment, operation, and maintenance of the software system contained within it. (p.136)
- Good architecture make 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. (p. 137)
- To be effective, a software system must be deployable. The higher the cost of deployment, the less useful the system is. A goal of a software architecture, then, should be to make a system that can be easily deployed with a single action. (p. 138)
- The primary cost of maintenance is in spelunking and risk. Spelunking is the cost of digging through the existing software, trying to determine the best place and the best strategy to add a new feature or to repair a defect. While making such changes, the likelihood of creating inadvertent defects is always there, adding to the cost of risk. (p. 139)
- All software systems can be decomposed into two major elements: policy and details. The policy element embodies all the business rules and procedures. The policy is where the true value of the system lives. The goal of the architect is to create a shape for the system that recognizes policy as the msot essential element of the system while making the details irrelevant to that policy. This allows decisions about those details to be delayed and deferred. (p. 140)
- A good architect maximizes the number of decisions not made. (p. 141)
Independence
- Support the use cases – The architecture of the system must support the intent of the system
- Support the operation – Architecture plays a more substancial, less cosmetic, role in supporting the operation of the system
- if the system needs to handle 1000,000 customers per second, the architecture has to handle that
- Basic good architecture actually allows for flexibility naturally for this
- Support the development environment
- Conway’s law = Any organization that designs a systemwill produce a design whose structure is a copy of the organization’s communication structure
- Monolithic team = monolithic application
- Independent teams = independent components working together
- Support the deployment
- The goal is “immediate deployment”
- Doesn’t rely on dozens of little configuration scripts and property file tweaks
- No manual creation of files or running scripts
- Leaving options open
- Use cases will change, are unknown, or will be added in the future – the system needs to be open to allow for those changes
- Some principles of architecture are relatively inexpensive to implement and can help balance those concerns
- Good architecture makes the system easy to change, in all the ways that is must change, by leaving options open
- Decoupling layers
- Horizontal layers – We will find a system with good architecture to be divided into decouple horizontal layers – UI, app-specific business rules, app-independent business rules, database, etc…
- Vertical Layers – Use cases are narrow vertical slices that cut through the horizontal layers of the system. Each use case uses some UI, some app-specific business rules, some app-independent business rules, and some database functionality.
- These vertical slice use cases also need to be decoupled. So use cases are limited to the horizontal layer. This way each use case can be handled by those independently (decoupled) modules.
- Decoupling mode
- If we apply decoupling to the operations area, we can find ourselves making each of these horizontal layers a “service”
- This is where service-oriented architecture or micro-service architecture comes from = decoupling the operations
- Remember, good architecture supports OPTIONS so whether it is a monolithic app or a micro-service app, either option will be available.
- Source level (separate code bases), deployment level (versioned libraries), or service level (micro-services)
- “My preference is to push the decoupling to the point where a services could be formed. Should it be necessary. But then to leave the components in the same address space as long as possible. This leaves the option for a service open… A good architecture will allow a system to be born as a monolith, deployed in a single file, but then 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.”
- Duplication
- True duplication is where every change to one instance necessitates the same change to every duplicate of that instance. These should be unified.
- False or accidental duplication is where two apparently duplication sections of code evolve along different paths and appear to be duplicates. These should not be unified.
- Imagine two use cases that have very similar screen structures. The architects will likely be strongly tempted to share the code for that structure. But should they? is that true duplication or is it accidental? Most likely it is accidental. As time goes by, the odds are that those two screens will diverge and eventually look very different. For this reason, care must be taken to avoid unifying them otherwise separating them later will be a challenge. (p. 155)
Boundaries: Drawing Lines
Most systems, other than monoliths, use more than one boundary stragegy. A system that makes use of service boundaries may also have some local process boundaries. Indeed, a service is often just a facade for a set of interacting local processes. A service, or local process, will almost certainly be either a monolith composed of source code components or a set of dynamically linked deployment components.
Policy and Level
- Policy relates to groups of business rules that are together
- Policies are organized and grouped so that their dependencies are a directed acyclic graph (see ADP and DAG)
- “Level” refers to the distance of a policy from inputs and outputs. Policies that manage input and output are the lowest-level policies in the system.
- Policies that change for the same reasons and at the same times are grouped together by the SRP and CCP.
Business Rules
- Critical business rules – rules or procedures that would exist even if there were not system to automate them
- Critical business data – data that would exist even if the system were not automated
- Entities = object in the system with a small set of rules operating on data
- Use case = description of the way that an automated system is used. Application-specific business rules
- Use cases are objects
- Use cases have their own request (input) and response (output) models
- These models should have no dependencies
- It is tempting to have these models reference the Entity objects. Avoid the temptation as their purpose is very different.
Screaming Architecture
- Good architectures are centered on use cases so that architects can safely describe the structures that support those use cases without committing to frameworks, tools, and environments
- Frameworks are options to be left open
- The web is an IO device and should be treated as such in architecture
- Frameworks are tools, not ways of life
- Look at each framework with a jaded eye. View it skeptically.
- Unit tests – if architecture is all about use cases, we should be able to unit test without the need of any of the frameworks in place
The Clean Architecture
- Various ideas on architecture systems over the decades: Hexagonal Architecture, DCI, BCE
- Each share these characteristics
- Independence of frameworks – does not depend on the existence of some library of feature-laden software
- Testable – the business rules can be tested without the UI, database, web server, etc…
- Independent of the UI – UI can change easily or could be replaced by another interface
- Independent of the database – should be able to swap out the database without breaking business rules
- Independent of any external agency – business rules don’t know anything about the outside world
The Dependency Rule
Source code dependencies must point only inward, toward higher-level policies.
- In the diagram, the outer circles are mechanisms and the inner circles are policies.
- Nothing in an inner circle can know anything at all about something in an outer circle.
Entities
- Entities encapsulate enterprise-wide Critical Business Rules.
- Data and functions that encapsulate the most general and high-level rules
- They are least likely to change when something external changes
Use Cases
- Contains the application-specific business rules
- Orchestrate the flow of data to and from the entities and direct those entities to use their critical business rules to achieve the goals of the use case
- We do expect changes in use cases over time, so the code in this layer is expected to change
Interface Adapters
- Convert data from the format most convenient for the use cases and entities to the format most convenient for some external agency such as the database or the web.
- This layer, for example will wholly contain the MVC architecture of a GUI
- No database code or any reference to external agencies should move from this level inward to the use cases
Frameworks and Drivers
- Contains the frameworks and tools such as the database and web framework
- Not much code is written other than glue code that communicates to the next circle inward
Crossing Boundaries
- When interaction needs to happen between the Interface Adapters and Use Cases, the use cases will code against an interface. The Interface Adapter code will then pass and implement the concrete class of that interface. This is an example of the Dependency Inversion Principle.
Which data crosses the boundaries?
- We use only simple data structures across boundaries
- We don’t send “database rows” or classes that have business logic
Presenters and Humble Objects
- Humble Object Pattern – design pattern to identify the way to help unit tester to separate behaviors that are hard to test from behaviours
- Humble module contains the hard-to-test behaviors stripped down to their barest essence
- The other model contains all the testable behaviors that are stripped out of the humble object
- View = humble object that is hard to test (HTML, GUI, etc…)
- Presenter = the testable object with basic, already formatted to be passed to the View for presentation
- If a date is to be displayed, the Presenter won’t just pass a Date object, but a formatted string of a date in just the way it should be displayed on the page
- If a price in money is to be displayed, the Presenter won’t just pass a Currency object, but will save a string that has the appropriate decimal places and currency markers to be displayed on the page
- Database Gateways – interfaces that contain methods for every create, read, update, and delete operation
- Gateway interfaces connect the use cases with external devices. Can be mocked for testing
- Data Mappers – this is where ORM’s would fall. ORMs form another kind of Humble Object between gateway interfaces and the database
- Service Listeners – Again we can use Humble Objects to represent services. We can then create service interfaces in the use cases.
Partial Boundaries
- Full-fledged architectural boundaries are expensive – polymorphic boundary interfaces, input and output data structures, and all the dependency management necessary to isolate the two sides into independently compilable and deployable components.
- Skip the Last Step – do all the work necessary to create independent compilable and deployable components and then simply keep them together to be built and deployed as one
- One-Dimensional Boundaries – Reciprocal boundary interfaces are typically used in full-fledged architectures. Utilizes the Strategy design pattern. A implementation of a service boundary is aware of both sides but it is up to the developer to only utilize that implementation one direction
- Facades – The dependency inversion is sacrificed for a Facade design pattern. The boundary is simply defined by the Facade class which lists all the services as methods and deploys the service calls to the classes that the client isn’t suppose to access.
Services: Great and Small
- False! Using services by their nature is architecture. It could just become expensive function calls across the system.
- Service benefits?
- The Decoupling Fallacy – Just by breaking into services a system is decoupled
- The Fallacy of Independent Development and Deployment – more services means more independence on releasing parts of a system while other parts stay the same
- Component-based Services
- 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 Missing Chapter (by Simon Brown)
- Substandard packaging ideas
- Package by Layer – horizonal layers, one package for UI, one package for business logic, and one package for persistence (i.e. database)
- Package by Feature – vertical slices of everything needed for a specific feature are in one package
- Package by Ports and Adapters
- Packaging is based on the clean architecture – core domain and external infrastructure
- All dependencies move inward to the core domain
- Remove ideas of naming things “Order Repository” and instead use real-world terms such as “Orders”
- Package by Component
- Hybrid approach to everything we’ve seen bundling all the responsibilities related to a single course-grained component into a single package
- In this, the UI is packaged separately, but the code relating to a feature (both business logic and persistence) is packaged together.
From left to right in image above. Green boxes are the packages:
- Package by Layers
- Package by Features
- Package by Ports and Adapters (clean architecture)
- Package by Components
Comments