Clean Architecture

Foreword

Preface

Introduction

Ch 1: What is Design and Architecture?

Behavior vs. Architecture

There are two values that software provides to stakeholders: (1) behavior, and (2) architecture. Behavior is the obvious one: that the software fulfills the functional requirements. Architecture is more often neglected. It is building the system in a way that keeps software soft, meaning it's easy to change. "The difficultly in making such a change should be proportional only to the scope of the change, and not to the shape of the change."

It's more important to have a program that doesn't work but that can change, than to have one that works but which is impossible to change. The reason is that if you can change the one that doesn't work, you can make it work. But if you can't change it at all, then it won't work to adapt to new requirements.

Business managers are not equipped to understand the importance of architecture. It's harder for them to see how poor architecture starts to slow you down. That's what software developers were hired to do. So it's up to the developer to assert the importance of architecture over the urgency of features. You have to see the long game and understand that all things considered, you will produce more with a better architecture. It's similar to getting out of the mindset of "we don't have enough time to test."

It's always a struggle between the development team fighting for what they think is best for the company, versus management doing the same thing, and marketing, and sales, etc.

Good developers unabashedly squabble with other stakeholders as equals over architecture. If architecture comes last, the system will become costlier and costlier to develop.

Ch. 5: Object-Oriented Programming

Encapsulation?

Inheritance?

Polymorphism?

Dependency Inversion

Ch. 6: Functional Programming

Event Sourcing


Part III: Design Principles

Ch. 7: SRP: The Single Responsibility Principle

Least well understood principle. Not "every module should do just one thing" (although there is a principle like that). The main thing to keep in mind here is that it's literally about avoiding having two devs work in the same file to fulfill requests for different actors.

Historical description: "A module should have one, and only one, reason to change." The "reason to change" is users and stakeholders. So, worded another way: "A module should be responsible to one, and only one, user or stakeholder."

But there will likely be more than one actual stakeholder or user that wants the system changed in the same way. So we can use the word "actor" to refer to a group of users or stakeholders, resulting in the final version:

A module should be responsible to one, and only one, actor.

Module = a source file.

Easier to understand principle if you look at symptoms of violating it.

Symptom 1: Accidental Duplication

class Employee {
  calculatePay() {}

  reportHours() {}

  save() {}
}

Putting these methods in the same class couples the three actors, so that the actions of one team could affect another. If the methods rely on shared functionality, then a request from one actor might require a change to the shared functionality, which will break the experience for another actor without them even knowing about the change.

Separate the code that different actors depend on.

Symptom 2: Merges

Different actors could request changes to the same source file for different reasons, resulting in a merge conflict between teams.

Solutions

class PayCalculator {
  calculatePay(employeeData) {}
}

class HourReporter {
  reportHours(employeeData) {}
}

class EmployeeSaver {
  save(employeeData) {}
}

// If you don't want to instantiate and keep track of a bunch of separate classes,
// you can reunify them via a facade.
class EmployeeFacade() {
  save(employeeData) {
    employeeSaver.save();
  }
  // etc.
}

Additional Notes

This works well in Node.js, since each file is actually called a module. Basically, make sure that each module is only responsible to one actor, and one actor only.

Ch. 8: OCP: The Open-Closed Principle

A software artifact should be open for extension but closed for modification.

Example: Financial Reporting Software

(See diagram on p. 72)

All component dependency relationships are unidirectional. The dependency arrows point towards the components we want to protect from change.

"If component A should be protected from changes in component B, then component B should depend on component A."

"Architects separate functionality based on how, why, and when it changes, and then organize that separated functionality into a hierarchy of components. Higher-level components in that hierarchy are protected from the changes made to lower-level components."

In other words, the key to architecture is optimizing for change.

Transitive Dependencies

Transitive dependencies are a violation of the general principle that software entities should not depend on things they don't directly use.

Ch. 9: LSP: The Liskov Substitution Principle

Ch. 10: ISP: The Interface Segregation Principle

If class A only calls one method of class B, but it references a concrete instance of class B, then class A will need to be recompiled every time a change is made to any methods on class B—even methods that class A doesn't call. But if you have class B implement an interface that class A uses, you only need to recompile class A if you change the interface. Better yet, if you separate out interfaces (instead of just one huge interface for class A), then there's an even smaller chance that B will need to recompile.

Examples

In this scenario below, every time I make a change to Ops.op3, I need to recompile User1, User2, and User3:

class Ops {
  op1() {}
  op2() {}
  op3() {}
}

class User1 {
  constructor(private ops: Ops) {}

  exec() {
    this.ops.op1();
  }
}

class User2 {
  constructor(private ops: Ops) {}

  exec() {
    this.ops.op2();
  }
}

class User3 {
  constructor(private ops: Ops) {}

  exec() {
    this.ops.op3();
  }
}

The interface segregation principle tells me to use separate interfaces, so that User1 doesn't need to depend on Ops.op3 if it doesn't call it:

interface Op1 {
  op1(): void;
}

interface Op2 {
  op2(): void;
}

interface Op3 {
  op3(): void;
}

class Ops implements Op1, Op2, Op3 {
  op1() {}
  op2() {}
  op3() {}
}

class User1 {
  constructor(private ops: Op1) {}

  exec() {
    this.ops.op1();
  }
}

class User2 {
  constructor(private ops: Op2) {}

  exec() {
    this.ops.op2();
  }
}

class User3 {
  constructor(private ops: Op3) {}

  exec() {
    this.ops.op3();
  }
}

This business of forcing recompiling is not an issue in JavaScript, Ruby, and Python, because they are dynamically typed. This allows them to be more loosely coupled than statically typed languages. So why care about this rule?

There's a theme here that touches on architecture, not just code compilation. The theme is that in general, it is harmful to depend on modules that contain more than you need.

Ch. 11: DIP: The Dependency Inversion Principle

"The most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions."

In other words, your import statements should refer only to modules containing interfaces, and nothing concrete.

This principle doesn't apply to concrete elements that are very stable, like built-in types in your language, but to concrete elements that frequently change.

Every change to an interface corresponds to a change in the implementation of the interface, but the opposite is not true. You can make all kinds of changes to the class without changing the interface.

The flow of control goes in the opposite direction of the source code dependencies. This is why it's called the Dependency Inversion Princple. The concrete classes depend on the abstractions, but flow of control is passed to the concrete classes.

Coding practices from this principle

Factories

In general, you should try to limit the creation of volatile objects to low-level detail files, like the main function or the index file in JavaScript. Here's a code example that mirrors the diagram he shows in the book on page 90:

index.ts

This is the file where everything comes together. It is the main function that creates the concrete implementations.

import { Application } from './application';
import { ServiceFactory } from './service-factory';
import { ServiceFactoryImpl } from './service-factory-impl';

const serviceFactory: ServiceFactory = new ServiceFactoryImpl();

const app = new Application(serviceFactory);

app.service.doThing();

application.ts

This is where all the business rules live, so this file shouldn't contain any imports of concrete classes. It only imports interfaces. It receives concrete objects at runtime via its constructor.

import { Service } from './service';
import { ServiceFactory } from './service-factory';

export class Application {
  service: Service;

  constructor(private serviceFactory: ServiceFactory) {
    this.service = this.serviceFactory.makeSvc();
  }
}

service-factory.ts

Interface. Only depends on the service interface.

import { Service } from './service';

export interface ServiceFactory {
  makeSvc(): Service;
}

service.ts

export interface Service {
  doThing(): void;
}

service-factory-impl.ts

This violates the DIP because it imports a concrete class. This is normal and necessary. You have to declare concrete classes at some point, but the goal of the principle is to not have your higher-level classes rely on concretions.

import { Service } from './service';
import { ServiceFactory } from './service-factory';
import { ConcreteImpl } from './concrete-impl';

export class ServiceFactoryImpl implements ServiceFactory {
  makeSvc(): Service {
    return new ConcreteImpl();
  }
}

concrete-impl.ts

import { Service } from './service';

export class ConcreteImpl implements Service {
  doThing(): void {
    console.log('it did the thing');
  }
}

Concrete Components

"DIP violations cannot be entirely removed, but they can be gathered into a small number of concrete components and kept separate from the rest of the system."

The best place for this kind of code is in the main function.

Part IV: Component Principles

SOLID principles tell us how to arrange bricks into walls; component principles tell us how to arrange rooms into buildings.

Ch. 13: Component Cohesion

The Reuse/Release Equivalence Principle

Components should have an overarching theme or purpose that all of its classes and functions share, such that it should make sense to release the whole thing as a package version.

The Common Closure Principle

"Gather into components those classes that change for the some reasons and at the same times. Separate into different components those classes that change at different times and for different reasons."

This is the Single Responsibility Principle, applied to components. A component should not have multiple reasons to change.

The Common Reuse Principle

"Don't force users of a component to depend on things they don't need."

When a class in one component uses a class in another component, there is a dependency. Component A will need to change or at least be redeployed when Component B changes, even if Component A doesn't care about the changes in Component B.

The goal is that if you have one component depending on another, you depend on all of the classes in that component.

It should be impossible to depend on some classes in a component and not others.

It's the Interface Segregation Principle, applied to components.

Ch. 14: Component Coupling

The Acyclic Dependencies Principle



The Stable Dependencies Principle

Depend in the direction of stability.


Skipping way ahead to the actual parts of the clean architecture...


Ch. 20: Business Rules

Example entity

class Loan {
  principle: number;
  rate: number;
  period: number;
  makePayment() {};
  applyInterest() {};
  chargeLateFee() {};
}

"Entity is pure business and nothing else."

It doesn't need to be OOP. All that is required is that you put data and business logic in the same module.

Use Cases

Example Use Case

Gather Contact Info for New Loan

Input: Name, Address, Birthdate, DL, SSN, etc. Output: Same info for read back + credit score.

Primary Course:

  1. Accept and validate name.
  2. Validate address, birthdate, DL, SSN, etc.
  3. Get credit score
  4. If credit score < 500 activate Denial.
  5. Else create Customer and activate Loan Estimation.

Request and Response models

Ch. 21: Screaming Architecture

"If your architecture is based on frameworks, then it cannot be based on use cases."

Ch. 22: The Clean Architecture

Entities

Use Cases

Interface Adapters

Frameworks and Drivers

Crossing Boundaries

In the example diagram, the flow of control goes:

Controller -> Use Case -> Presenter

But even though the flow of control moves from the use case to the presenter, the use case doesn't know about the presenter. The use case doesn't call the presenter; it calls an interface that the presenter implements.

Which Data Crosses the Boundaries

A Typical Scenario

  1. Web server gathers input data from user, and hands it to the Controller
  2. Controller formats data into plain object and passes it through the InputBoundary to the UseCaseInteractor
  3. The UseCaseInteractor...
  4. interprets the data and uses it to control the dance of the Entities
  5. Uses the DataAccessInterface to get the data uses by the Entities from the Database and into memory
  6. The UseCaseInteractor gathers data from the Entities and constructs the OutputData as another plain object.
  7. The UseCaseInteractor passes the OutputData through the OutputBoundary interface to the Presenter.
  8. The Presenter packages the OutputData into viewable form as the ViewModel (another plain object). (Transforms dates and currency into string format ready to be displayed.)
  9. The View moves the data from the ViewModel into the HTML page.

Ch. 23: Presenters and Humble Objects

The Humble Object Pattern

The idea is that you separate hard-to-test behaviors from easy-to-test behaviors, and put them in separate modules. Then you can test the one with the easy-to-test behaviors.The "humble object" is the module that is hard to test.

Presenters and Views

Testing and Architecture

Database Gateways

Conclusion

At each architectural boundary we are likely to find the Humble Object pattern nearby.

Using this pattern vastly increases the testability of the system.

Ch. 24: Partial Boundaries