Alex BespoyasovAuthor's photo

The Clean Architecture by Robert C. Martin

I split the summary of this book in 3 parts. In the first one, we will discuss the concept of architecture, an overview of programming paradigms, and an explanation of the SOLID principles.

Preface and Introduction

TL;DR:

  • The rules of architecture are the same for any software system.
  • Difficulties in support arise because of poor architecture.

There are different paradigms and techniques, but ultimately everything converts to conditions, assignments, and loops. And that's why the rules of architecture are universal.

It's not hard to make code work. It's hard to develop and maintain it. If the software is done correctly, then making changes or adding functionality isn't painful or quick. And vice versa, if the software is not built correctly, the amount of effort to work increases manifold.

Chapter 1. Design and Architecture

TL;DR:

  • The goal of a good architecture is to minimize the resources needed to support and improve software.
  • Bad code slows down development not only in the future, but right now.

There is no difference between design and software architecture. Using the example of an architect (not software): they think about the technical aspects of a building, but they also describe how the building looks.

The goal of good architecture is to minimize the resources needed to support and improve software.

With a bad architecture, increasing the number of developers will not help. The motivation of the developers will fade: they will see that they work as much, but the performance drops.

The “we'll clean it up later, roll out the feature first” approach doesn't work, because there's always another feature to ship. And bad code slows down development. The only way to develop fast is to develop well.

Chapter 2. Tale of Two Values

TL;DR:

  • Developers are responsible for two aspects of software: its behavior and structure.
  • Good architecture does not depend on any system model invented by developers.
  • Maintainability in the future is more important than compliance right now.

Developers are responsible for two aspects of software: its behavior and structure. Behavior is how the software meets business needs. Developers are hired and paid money for that behavior. But to think that only behavior matters is not seeing the big picture.

In the term software there is a part of soft which means that the system must be “soft”, easily changeable. If each change contradicts previous changes, does not fit into the model already created, it will be harder to make changes in the future. A good architecture doesn't depend on any model and doesn't favor any of them.

Which is more important? Using examples:

  1. If there is a program that perfectly meets the requirements, but it cannot be maintained, when new requirements come in, it will be impossible to meet them. Consequently, the program will not change, and therefore will become useless.
  2. If there is a program that doesn't work quite right, but it is easy to change, then it is also easy to bring it into compliance. So it will remain useful.

Developers will have to prove the usefulness of refactoring and improving the architecture.

Chapter 3. Paradigms Overview

TL;DR: structured programming, object-oriented programming, functional programming—all paradigms forbid something. Paradigms know exactly and tell us what not to do.

Chapter 4. Structured Programming

TL;DR:

  • Understanding the program completely is difficult, the program must be broken down into smaller peaces.
  • Erratic direct control (goto) is bad.
  • Tests help to find and prove that there is a bug. They do not prove that there are no bugs.

Dijkstra observed that understanding a program completely is difficult, and goto prevents it from being broken down into smaller parts. In contrast, simple control constructions (like if/else, do/while) made the work easier. Any program can be built from such elementary control constructions.

Structured programming allowed a program to be broken down into parts that could be recursively broken down into smaller parts.

Also, Dijkstra pointed out that tests help find and prove that there is a bug. But they don't prove that there are no bugs. In this, programming is more like physics than mathematics. Mathematics proves that a statement is true, while physics cannot do this and tries to prove that the statement is false.

Chapter 5. Object-Oriented Programming

TL;DR:

  • OOP: encapsulation, polymorphism, inheritance?
  • Make the important things kernel and everything else plugins to it.

Encapsulation means that you can draw a line around a function or dataset so that you can't see the data inside from the outside. Encapsulation exists not only in OO languages.

Inheritance is basically just a re-definition of some fields or methods. It also doesn't only exist in OO languages, although inheritance works better there.

Polymorphism also predates OO, but OO languages have made it safer and more convenient. The idea of polymorphism helped develop plug-in architecture and the understanding that any dependency can be inverted.

A plug-in architecture means that the UI and the database should depend on business rules, not the other way around. The UI and the database are plugins, add-ons to the business rules and depend on them. This means that the business rules code knows nothing about the UI or the database, it has nothing to do with them at all.

The UI and the database depend on business rules, not the other way around
The UI and the database depend on business rules, not the other way around

Business rules code can change and roll out in extensions regardless of anything else.

Chapter 6. Functional Programming

TL;DR: data immutability is good.

FP teaches the data immutability. But there are a few compromises. For example, having both pure components and components with side effects in the application.

FP suggests the idea of event sourcing, when we do not store state, but store transactions (transitions between states). To get a state at some point in time, we need to apply to the initial state all the transactions that occurred at the desired point in time.

Chapter 7. Single Responsibility Principle

TL;DR: a function should solve only one problem.

A function should solve only one problem, and a module should have only one reason to change it. You should separate code that changes for different reasons into different files.

It is worth dividing entities so that they are minimal. You can use facades to combine complex logic into one method.

Chapter 8. Open Closed Principle

TL;DR:

  • The behavior of the entity must be extensible without having to change the entity itself.
  • If component A must be protected from changes in component B, then component B must depend on component A.

Example: let's say there is a thing that generates a bank report for the web. There may be negative numbers in the report and they are marked red. The page scrolls if there are a lot of lines.

Now we are asked to make the same report only for black and white printing. The negative numbers there should be in brackets, and the report should be paginated. In order to change as little code as possible, it should be broken down by responsibilities: there is report generation and there is data output.

About dependencies: if component A must be protected from changes in component B, then component B must depend on component A. This is the only way to build relationships between components such that changes in requirements will not gut the entire application.

Chapter 9. Liskov Substitution Principle

TL;DR: functions that use a basic type must be able to use subtypes of the basic type without knowing it.

A typical example of a violation of the principle: suppose there is a class Rectangle, from it the class Square is inherited. This is not exactly true, because a rectangle can change sides independently, while a square should change sides together. The only way to know externally which class an object is an instance of is to ask it directly.

Chapter 10. Interface Segregation Principle

TL;DR: dependence on something you don't actually use can cause problems you don't foresee.

If you imagine a situation where User1 depends only on op1, the first architecture of the first diagram can lead to unnecessary recompilations:

Direct inheritance can lead to recompilation
Direct inheritance can lead to recompilation

The architecture in the second diagram solves this problem:

Inheritance via interface
Inheritance via interface

Chapter 11. Dependency Inversion Principle

TL;DR:

  • Modules should depend on abstractions, not implementation details.
  • Abstractions should not depend on implementation details.

A change in abstraction leads to a change in implementation. But not every change in implementation must lead to a change in abstraction. Thus interfaces are less changeable than implementations. So don't be bound to specific classes, instead be bound to interfaces.

Next Posts

In the second part, we'll talk about coupling and cohesion of system components, and discuss in more detail the goals of a good architecture.

In the third part, we'll discuss business rules, architecture levels, and talk a bit about templates and tests.

Previous post: Copypaste in CodeNext post: There's Always Time to Read Books—2