Clean Architecture
These are my notes from Clean Architecture: A Craftsman's Guide to Software Structure and Design Book by Robert C. Martin (AKA uncle BOB).
Part 1: Introduction
#1 What is Design and Architecture?
- The word Architecture get a lot of misunderstanding: it is often used in the context of something at a high level details (totally divorced from the low level details).
- The low-level and the high-level decisions are part of the whole. They form continuous fabric that defines the shape of the system, you can't have one without the other.
The goal of software architecture is to minimize the human resources required to build and maintain the required system.
- The best option is for the development organization to recognize and avoid its own overconfidence and to start taking the quality of its software architecture seriously.
#2 A Tale of Two Values
- Every software system provides 2 different values to the stakeholders:
- Behaviour
- Architecture/Structure Unfortunately, software developers either focus on one to the exclusion of the other or focus on the lesser of the two values.
Behaviour
- Programmers make machines behave in a way that makes or saves money for the stakeholders by helping the stakeholders develop a functional specification, or requirements document.
- Then they write the code that causes the stakeholder's machines to satisfy those requirements
- When the machine violates those requirements, programmers get their debuggers out and fix the problem.
Architecture
- Software must be easy to change. The difficulty in making such a change should be proportional only to the scope of the change, and not to the shape of the change.
- The problem is the architecture of the system. The more it prefers one shape over another, the more likely new features will be harder and harder to fint into that structure.
The Greater Value
-
There are systems that are practically impossible to change, because the cost of change exceeds the benefit of change.
→ The Greater value is the Architecture.
Eisenhower's Matrix
- Those things that are urgent are rarely of greate importance, and those things that are important are seldom of great urgency.
- Behavior is urgent but not always particularly important.
- Architecture is important but never particularly urgent.
Part 2: Starting with the Bricks: Programming Paradigms
#3 Pradigm Overview
Structured Programming
- Discovered by Edsger Wybe Dijsktra in 1968, He showed that the use of goto statements is harmful to program structure.
Structured programming imposes discipline on direct transfer of control.
Object-Oriented Programming
- Discovered two years earlier in 1966 by Ole Johan Dahl and Kristen Nygaard, They noticed that the function call stack frame in the ALGOL language could be moved to a heap, thereby allowing local variables declared by a function to exist long after the function returned.
Object-oriented programming imposes discipline on indirect transfer of control.
Functional Programming
- Has only recently begun to be adopted, was the first to be invented and the direct result of the work of Alonzo Church.
- A functional language has no assignment statement.
Functional programming imposes discipline upon assignment.
#4 Structured Programming
- Dijkstra realized that these “good” uses of goto corresponded to simple selection and iteration control structures such as if/then/else and do/while. Modules that used only those kinds of control structures could be recursively subdivided into provable units.
- The very control structures that made a module provable were the same minimum set of control structures from which all programs can be built. Thus structured programming was born.
- Structured programming allows modules to be recursively decomposed into provable units, which in turn means that modules can be functionally decomposed. That is, you can take a large-scale problem statement and decompose it into high-level functions. Each of those functions can then be decomposed into lower-level functions, ad infinitum.
- Structured programming forces us to recursively decompose a program into a set of small provable functions. We can then use tests to try to prove those small provable functions incorrect. If such tests fail to prove incorrectness, then we deem the functions to be correct enough for our purposes.
Software architects strive to define modules, components, and services that are easily falsifiable (testable). To do so, they employ restrictive disciplines similar to structured programming, albeit at a much higher level.
#5 Object-Oriented Programming
- Object-Oriented is "the combination of data and function" or "a way to model the real world".
#6 Encapsulation
- OO languages provide ease and effective encapsulation of data and function.
- The way encapsulation is partially repaired is by introducticing the public, private, and protected keywords into the language.
OO certainly does depend on the idea that programmers are well-behaved enough to not circumvent encapsulated data. Even so, the languages that claim to provide OO have only weakened the once perfect encapsulation we enjoyed with C.
Inheritance
- Inheritance is simply the redeclaration of a group of variables and functions within an enclosing scope. This is something C programmers were able to do manually long before there was an OO language.
We can award no point to OO for encapsulation, and perhaps a halfpoint for inheritance. So far, that’s not such a great score. But there’s one more attribute to consider.
Polymorphism
- polymorphism is an application of pointers to functions. Programmers have been using pointers to functions to achieve polymorphic behavior since Von Neumann architectures were first implemented in the late 1940s.
- OO languages may not have given us polymorphism, but they have made it much safer and much more convenient.
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.
#6 Functional Programming
- Variables in functional languages do not vary.
- All race conditions, deadlock conditions, and concurrent update problems are due to mutable variables. You cannot have a race condition or a concurrent update problem if no variable is ever updated. You cannot have deadlocks without mutable locks.
- All the problems we face in applications that require multiple threads, and multiple processors cannot happen if there are no mutable variables.
- Structure programming is discipline imposed upon direct transfer of control.
- Object-oriented programming is discipline imposed upon indirect transfer of control
- Functional programming is discipline imposed upon variable assignment.
The rules of software are the same today as they were in 1946,
when Alan Turing wrote the very first code that would execute in an electronic computer.
The tools have changed, and the hardware has changed, but the essence of software remains the same.
Software is composed of sequence, selection, iteration, and indirection.
Nothing more. Nothing less.
Part 3: Design Principles
- The S.O.L.I.D principles tell us how to arrange our functions and data structures into classes, and how those classes should be interconnected.
- A class is simply a coupled grouping of functions and data. The SOLID principles apply to those groupings.
The Single Responsibility Principle (SRP)
An active corollary to Conway's law:
The best structure for a software system is heavily influenced by the social structure of the organization that uses it so that each software module has one, and only one, reason to change.
The Open-Closed Principle (OCP)
For software systems to be easy to change, they must be designed to allow the behavior of those systems to be changed by adding new code, rather than changing existing code.
The Liskov Substitution Principle (LSP)
To build software systems from interchangeable parts, those parts must adhere to a contract that allows those parts to be substituted one for another.
The Interface Segregation Principle (ISP)
Software designers should avoid depending on things that they don't use.
The Dependency Inversion Principle (DIP)
The code that implements high-level policy should not depend on the code that implements low-level details. Rather, details should depend on policies.
#7 The Single Responsibility Principle (SRP)
A module should be responsible to one, and only one, actor.
- 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 Principle
. - At the level of architecure, it becomes
the Axis of Change
responsible for the creation of Architectural Boundaries.
- At the level of components, it becomes
#8 The Open-Closed Principle (OCP)
A software artifact should be open for extension but closed for modification.
- The behavior of a software artifact ought to be extendible, without having to modify that artifact.
- The 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.
#9 The Liskov Substitution Principle (LSP)
- 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.
#10 The Interface Segregation Principle (ISP)
- Depending on something that carries baggage that you don't need can cause you trouble that you didn't expect.
#11 The Dependency Inversion Principle (DIP)
- The most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.
- Don't refer to volatile concrete classes, Refer to abstract interfaces instead.
- Don't derive from volatile concrete classes.
- Don't override concrete functions.
- Never mention the name of anything concrete and volatile.
- The way the dependencies cross that curved line in one direction, and toward more abstract entities, will become a new rule that we will call the
Dpendency Rule
.
Part 4: Component Principles
SOLID principles tell us how to arrange the bricks into walls and rooms. Component principles tell us how to arrange the rooms into buildings.
#12 Components
- Components are the smallest entities that can be deployed as part of a system.
- Components can be linked together into a single executable, or they can be aggregated together into a single archive.
- The dynamically linked files, which can be plugged together at runtime, are the software components of architectures.
#13 Component Cohesion
REP (The Reuse/Release Equivalence Principle)
- People who want to reuse software components cannot/will not do so unless those components are tracked through a release process and are given release numbers.
- This not simply because without release numbers there would be noway to ensure that all the reused components are compatible with each other, but also because that software developers need to know when new releases are coming, and which changes those new releases will bring.
- The release process must produce the appropriate notifications and release documentation so that users can make informed decisions about when and whether to integrate the new release.
→ REP means that the classes and modules that are formed into a component must belong to a cohesive group, and there must be some overaching theme or purpose that those modules all share. - Classes and modules that are grouped together into a component should be reusable together. The fact that they share the same version number and the same release tracking, and are included under the same release documentation should make sense both to the author and to the users.
- This is a weak advice because it is hard to precisely explain the glue that holds the classes and modules together into a single component, But the principle itself is important, because violations are easy to detect/don't "make sense".
CCP (The Common Closure Principle)
- As the SRP (Single Responsibility Principle) says that a class shoudl not contain multiple reasons to change, so the CCP (Common Closure Principle) says that a component should not have multiple reasons to change.
- If the code in an application must change, you would have that all of the changes occur in one component, rather than being distributed across many components. And you need only to redeploy the one changed component.
- If two classes are so tightly bound, either physically or conceptually, that they always change together, then they belong in the same component. This minimizes the workload related to releasing, revalidating, and redeploying the software.
- When a change in requirements comes along, that change has a good chance of being restricted to a minimal number of components.
Gather together those things that change at the same times and for the same reasons.
Separate those things that change at different times or for different reasons.
CRP (The Common Reuse Principle)
- CRP states that classes and modules that tend to be reused together belong in the same component.
- Reusable classes collaborate with other classes that are part of the reusable abstraction. CRP states that these classes belong together in the same component. In such a component we would expect to see classes that have lots of dependencies on each other.
- CRP also tells us which classes not to keep together in a component. When one component uses another, a dependency is created between the components. Because of that dependency, every time the used component is changed, the using component will likely need corresponding changes.
- We want to make sure that the classes that we put into a component are inseparable that it is impossible to depend on some and not on the others.
- CRP tells us more about which classes shouldn't be together than about which classes should be together.
→ CRP says that classes that are not tightly bound to each other should not be in the same component. - While ISP advises us not to depend on classes that have methods we don't use, The CRP advises us not to depend on components that have classes we don't use.
Don't depend on things you don't need.
The Tension Diagram For Component Cohesion
- The three cohesion principles tend to fight each other: The REP and CCP are inclusive principles (both tend to make components larger). The CRP is an exclusive principle (driving components to be smaller)?
- Generally, projects tend to start on the CCP and CRP where the only sacrifice is reuse. As the project matures, and other projects begin to draw from it. the project wil slide to REP.
#14 Component Coupling
A problem occurs in development environments where many developers are modifying the same source files: weeks go by without the team being able to build a stable version of the project and everyone keeps on changing their code trying to make it work with the last changes that someone else made.
Two solutions to this problem have evolved: The weekly build and Acyclic Dependencies Principle.
The weekly build
- In the traditional weekly build cycle, developers work independently for four days before integrating changes on Fridays.
- As projects grow, the integration process spills into Saturdays, prompting a shift to earlier integration days.
- This compromises team efficiency, leading to a switch to a biweekly build schedule. However, challenges persist with project size, creating a crisis. Lengthening the build schedule increases risks, complicating integration and testing while undermining the benefits of rapid feedback.
Eliminating Dpendeny Cycles
- To address integration challenges, partition the development environment into releasable components. Developers or teams manage these components, releasing functional versions for others to use. Teams independently decide when to adopt new releases, avoiding immediate dependencies.
- This decentralized approach enables incremental integration, eliminating the need for a simultaneous, collective integration point.
- Managing the dependency structure is critical to prevent cyclical dependencies and associated integration issues.
Top-Down Design
- The component structure of a system evolves rather than being designed top-down. Contrary to expectations, component dependency diagrams primarily serve buildability and maintainability, not functional descriptions.
- They emerge as the system grows to manage dependencies, avoiding integration challenges. Architects use principles like SRP and CCP to collocate changing classes.
- The focus is on isolating volatility, preventing unstable components from impacting stable ones. The graph adapts with the application's growth, incorporating CRP for reusability and applying ADP to address cycles. Designing the component structure before classes is impractical, as it may overlook essential principles and result in dependency cycles.
The Stable Dpendencies Principle
- By conformiing to the CCP (Common Closure Principle), we create components that are sensitive to certain kinds of changes but immune to others.
- Any component that we expect to be volatile should not be depended on by a component that is difficult to change.
- By conforming the Stable Dependencies Principle (SDP), we ensure that modules that are intended to be easy to change are not depended on by modules that are harder to change.