Skip to content

Instantly share code, notes, and snippets.

@wataruoguchi
Last active May 27, 2021 04:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wataruoguchi/5a037de17ac283af3fdd24562b528dfd to your computer and use it in GitHub Desktop.
Save wataruoguchi/5a037de17ac283af3fdd24562b528dfd to your computer and use it in GitHub Desktop.
Clean Architecture - PART 3 - Design Principles #bookclub

Clean Architecture

PART III - Design Principles

The SOLID principles tell us how to arrange our functions and data structures into classes, and how those classes should be interconnected.

The goal of the principles is the creation of mid-level software structure that:

  • Tolerate change,
  • Are easy to understand, and,
  • Are the basis of components that can be used in many software systems.

The author is the one who made the SOLID principles!

Chapter 7 - SRP: The 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.

The opposition of this would be Shotgun surgery.

Symptom 1: Accidental Duplication

  • Fig 7.1 - The Employee class
class Employee {
  calculatePay() {}
  reportHours() {}
  save()
}

This violates the SRP because those methods are responsible to different actors.

  1. calculatePay is specified by the accounting department.
  2. reportHours is specified by HR department.
  3. save is specified by the database admins.

The module can be changed by a change of one of three departments. Three different dependencies.

Symptom 2: Merges

merges will be common in source files that contain many different methods.

Multiple developers check out the class and make changes. The changes collide. Merges are risky.

I think code discoverability is also one.

Solutions

Perhaps the most obvious way to solve the problem is to separate the data from the functions.

  • Fig 7.3 - The three classes do not know about each other

It's interesting that this sounds like an opposition of "encapsulation".

The downside of this solution is that the developers now have three classes that they have to instantiate and track.

-> Solution is to use the Facade pattern.

  • Fig 7.4 - The Facade pattern

Employee Facade is a thin module - it just calls the functions.

  • Fig 7.5 - The most important method is kept in the original Employee class and use a Facade for the lesser functions

Now it's coming back to "encapsulation", but details are separated.

Conclusion

The principle is about functions and classes.

  • At the level of components: The Common Closure Principle
  • At the level of architecture: Axis of Change responsible for the creation of Architectural Boundaries

Chapter 8 - OCP: The Open-Closed Principle

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

A Thought Experiment

Imaginary web system:

  • Displays a financial summary
  • The data is scrollable
  • negative numbers are in red

Imaginary request:

  • Print on a black-and-white printer.
  • Properly paginated.
  • Negative numbers should be surrounded by parentheses.

How much old code will have to change?

  • Fig 8.1 - Applying the SRP

[Financial Data] -> (Financial Analyze) -> [Financial Report Data] -> (Web Reporter) (Print Reporter)

Organize the code with classes and components.

  • Fig 8.2 - Participating the processes into classes and separating the classes into components

Decoupled by the following components;

  • Controller

  • Interactor

  • Database

  • Presenters

  • Views

  • Fig 8.3 - The component relationships are unidirectional

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

The interactor is protected from changes in everything else - that best conforms to the OCP. Interactor contains the business rules -> The highest-level policies of the application.

Data will change, how we represent will change, but the business does not change. Let's sort the list above by the hierarchy.

  1. Interactor
  2. Database
  3. Controller (Same level as #2)
  4. Presenters
  5. Views

Directional Control

If you see the diagram again, you see the arrows are pointed to correct directions, with interfaces defined. -> "Dependency Inversion" that you learned in Part 2.

Information Hiding

Use interface to hide implementation details.

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

Conclusion

The OCP is one of the driving forces behind the architecture of systems.

It's accomplished by partitioning the system into components and arranging them into a dependency hierarchy.

Chapter 9 - LSP: The Liskov Substitution Principle

What is wanted here is something like the following substitution property: If for each object O1 of type S there is an object O2 of type T that for all programs P defined in terms of T, the behavior of P is unchanged when O1 is substituted for O2 then S is a subtype of T.

Guiding the Use of Inheritance

  • Fig 9.1 - License, and its derivatives, conform to LSP
class Billing {
  private license: License;
  constructor(license: License) {
    this.license = license;
  }
  getFee() {
    return this.license.calcFee();
  }
}

// Interface for a class
abstract class License {
  abstract calcFee: () => number;
}

class PersonalLicense extends License {
  calcFee(): number {
    // Something
  }
}
class BusinessLicense extends License {
  users;
  calcFee(): number {
    // Something
  }
}
  • Billing: P
  • License: O2, type T
  • PersonalLicense: O1, type S (subtype of T)
  • BusinessLicense: O1, type S (subtype of T)

The behavior of Billing does not depend on any of those subtypes.

The Square / Rectangle Problem

Example of a violation of the LSP.

class User {
  shape: Rectangle;
  constructor(shape: Rectangle) {
    this.shape = shape;
  }
  shapeArea(width, height) {
    // EEK..
    if (this.shape instanceof Square) {
      // Square
      this.shape.setSide(width);
    } else {
      // Rectangle
      this.shape.setW(width);
      this.shape.setH(height);
    }
    return this.shape.area();
  }
}
class Rectangle {
  width;
  height;
  area() {
    return this.width * this.height;
  }
  setW(w) {
    this.width = w;
  }
  setH(h) {
    this.width = h;
  }
}
class Square extends Rectangle {
  setSide(s) {
    this.width = s;
    this.height = s;
  }
}

LSP and Architecture

It used to be for use of inheritance. Today, this is for use of interfaces and implementations.

Example LSP Violation

The LSP from an architectural viewpoint. Special cases for acme.com

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.

Chapter 10 - ISP: The Interface Segregation Principle

  • Fig 10.1 The Interface Segmentation Principle

Several users use the operations of OPS class. But the user1 only uses only OPS#op1, same goes for user2 and user3. The change of op2 will force user1 to be recompiled and destroyed.

  • Fig 10.2 Segregated Operations

Create the interface U1Opts that User1 depends on. The Interface describes part of OPS. Same goes for User2 and User3.

ISP and Language

Statically typed languages forces programmers to create declarations that users must import or use, or otherwise include. They are forming dependencies and changing dependencies means that it force recompilation and redeployment.

While in dynamically typed languages don't have such declarations because they're inferred at runtime.

This fact could lead you to conclude that the ISP is a language issue, rather than an architecture issue.

ISP and Architecture

In general, it is harmful to depend on modules that contain more than you need.

  • Fig 10.3 A problematic architecture

System S -> Framework F -> Database D

Now suppose that D contains features that F does not use and, therefore, that S does not care about. Changes to those features within D may well force the redeployment of F, and therefore, the redeployment of S.

Conclusion

The lesson here is that depending on something that carries baggage that you don't need can cause you troubles that you didn't expect.

Chapter 11 - DIP: The Dependency Inversion Principle

We tolerate those concrete dependencies because we know we can rely on them not to change.

Stable Abstractions

The implementation, then, is that stable software architectures are those that avoid depending on volatile concretions, and that favor the use of stable abstract interfaces. This implementation boils down to a set of very specific coding practices:

  • 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.

Factories

To comply with these rules, the creation of volatile concrete objects requires special handling.

  • Fig 11.1 Use of Abstract Factory pattern to manage the dependency
  1. The Application uses the ConcreteImpl through the Service interface.
  2. The Application calls the ServiceFactory#makeSvc to create a ConcreteImpl instance.
  3. The ServiceFactory#makeSvc is implemented by the ServiceFactoryImpl class, derives from ServiceFactory.

The line in Fig 11.1 is an architectural boundary. It separates the abstract from the concrete. ...(the line) divides the system into two components: one abstract and the other concrete.

  • The abstract component contains all the high-level business rules of the application.
  • The concrete component contains all the implementation details that those business rules manipulate.
class App {
  Run(SvcFactory: ServiceFactory) {
    const service: Service = SvcFactory.makeSvc();
    const serviceId: string = service.id();
  }
}

abstract class ServiceFactory {
  abstract makeSvc: () => Service;
}

abstract class Service {
  abstract id: () => string;
}

// ~~~~~ architectural boundary ~~~~~

class ConcreteImpl implements Service {
  constructor() {}
  id() {
    return "foo";
  }
}

class ServiceFactoryImpl implements ServiceFactory {
  public makeSvc(): Service {
    return new ConcreteImpl(); // <- DIP violation
  }
}

function main() {
  // It instantiates `ServiceFactoryImpl`
  App.Run(ServiceFactoryImpl);
}
main();

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.

Conclusion

DIP will be the most visible organizing principle. The curve line "architectural boundary" will become a new rule: Dependency Rule.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment