Rereading Clean Architecture (3): SOLID Design Principles

Tommy Cheese | · Reading time: 3 min

Rereading Clean Architecture (Part 3): The SOLID Principles

There are three main goals when building software modules:

  • Make software tolerant of change.
  • Make software easier to understand.
  • Build components that can be reused across multiple software systems.

The SOLID principles primarily tell us how to organize data and functions into classes and how to connect those classes into programs. They guide module design; other design principles apply at the architectural level.

SOLID refers to the Single Responsibility Principle, SRP; the Open–Closed Principle, OCP; the Liskov Substitution Principle, LSP; the Interface Segregation Principle, ISP; and Dependency Inversion, DIP.

Single Responsibility Principle (SRP)

SRP states that each software module should be responsible for a single function: there should be only one reason for a module to change. Each software module should be responsible to one kind of actor.

At the component level, SRP is known as the Common Closure Principle (CCP).

What happens when a module is designed without following SRP? Consider this example.

Suppose the finance, human resources, and engineering departments all depend on a payroll and working-hours program with three functions:

  • CalculateSalary: calculate salary.
  • CalculateTime: calculate working hours.
  • Save: save information.

The three departments use the program without incident until one day the finance department changes its salary calculation rules. Its maintainer updates CalculateSalary. Then something unexpected happens: salaries for human resources and engineering change as well…

Open–Closed Principle (OCP)

“Welcome additions; resist modifications!”

OCP states that a software system should allow its behavior to be changed by adding code, rather than only by modifying existing code. Well-designed software should be easy to extend while resisting modification.

OCP applies to components as well as classes and modules. It originated in the concept of device independence. Specifically, dependency inversion lets us design I/O devices as plugins, making it easy to add new devices without changing existing ones.

OCP is implemented by dividing a system into components and organizing their dependencies hierarchically so that changes in lower-level components do not affect higher-level components.

Liskov Substitution Principle (LSP)

LSP states that if interchangeable components are used to build a software system, they must follow the same contract so that one can substitute for another.

LSP is fundamentally a principle of substitutability: if, for every object o1 of type S, there is an object o2 of type T such that a program P operating on T behaves identically when o1 replaces o2, then S is a subtype of T.

LSP can and should be applied at the architectural level. Once substitutability is violated, a system architecture must introduce extensive, complicated mechanisms to compensate.

Interface Segregation Principle (ISP)

ISP states that unnecessary dependencies should be avoided. At any level of software design, depending on something that is not needed can introduce unexpected trouble.

Dependency Inversion Principle (DIP)

DIP states that code implementing high-level policy should not depend on code implementing low-level details. Instead, code implementing those details should depend on the high-level policy.

To understand dependency inversion, distinguish control flow from source-code dependency flow. Suppose control flows from component A to component B. In a direct implementation, A imports B, so the source-code dependency also points from A to B. System behavior determines control flow, which in turn dictates source-code dependencies, leaving the architecture little freedom. Introducing an interface changes this: B implements the interface, and A depends on the interface instead of importing B. Across that boundary, the source-code dependency is reversed relative to control flow. This is dependency inversion.

Dependency inversion gives architectural design freedom. A flexible system should depend more on abstractions—such as interfaces and abstract classes—than on concrete implementations.

DIP yields several coding guidelines:

  • Use abstract interfaces and avoid depending on volatile concrete implementation classes wherever possible.
  • Do not derive classes from concrete implementation classes.
  • Do not override functions that contain concrete implementations.
  • Avoid referring in code to the names of concrete implementations or other things likely to change.

(End of Part 3)

comments powered by Disqus