Introduction to Clean Architecture for TypeScript: PART2
Imamori Daichi
Posted on September 12, 2021
Refs
- Introduction to Clean Architecture for TypeScript: PART1
- Introduction to Clean Architecture for TypeScript: PART3
- Introduction to Clean Architecture for TypeScript: PART4
Design principles
In the book "Clean Architecture", before moving on to the discussion of architecture, there is an explanation of a design principle called SOLID.
SOLID principles are a set of principles that apply at the module level and provide guidance for connections between modules.
Although the design guidelines for the code level, which is the building block of a module, are not covered in this book, "Clean Code" by Uncle Bob or "The Art of Readable Code" may be helpful.
Adherence to the SOLID principles is a key element in achieving Clean Architecture.
In this book, we will also look back at the content of SOLID before discussing Clean Architecture in React applications.
SOLID is an acronym for the following five principles
- Single Responsibility Principle (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
In Clean Architecture, the SOLID principle is introduced as having the following three objectives
- Be resistant to change.
- Easy to understand.
- Be available for use in many software systems as a basis for components.
"Easy to understand" is a characteristic that is also required at the code level.
Then, by connecting modules that consist of well-written and clean code based on SOLID, the relationship between modules becomes easy to understand.
At first glance, "Be available for use in many software systems" tends to be used in situations where many users will use it, such as libraries and frameworks, but it can also be important in designing specific software.
As your business succeeds and your software grows, you will increasingly want to make certain modules available to other systems within your business.
For example, a service that was initially intended only for the web, but later grew to require native app development.
If you want to use some modules of your software written in React from React Native, you can minimize the modification for reuse if the SOLID is satisfied beforehand.
And "Be resistant to change" is a key feature in carrying out the objective of protecting the business domain from changes in other components.
In particular, the Dependency Inversion Principle (DIP) plays an important role in protecting the business domain from dependencies.
Let me introduce the five principles of SOLID in turn.
Single Responsibility Principle (SRP)
A module should have one, and only one, reason to change.
(Robert C, Martin. "SRP: The Single Responsibility Principle". Clean Architecture. Pearson Education.)
The term "actors" refers to a grouping of users and stakeholders.
For example, let's take an e-commerce service as an example.The following actors may be considered.
- Users who shopping
- Users on the store side who manage the products
- Stakeholders who determine the commission to the store side
- The team that decides on the sale launch
For these actors, make sure that one module depends on one actor.
In other words, SRP is the principle that no module should be affected by more than one actor.
The DRY (Don't repeat yourself) principle should also be considered when applying SRP.
I won't go into the details of the DRY principle itself, but in a nutshell, it is the principle that code that does the same thing should be put in one place.
If you try to practice DRY easily, you will end up sharing the same process of doing the same thing.
If you make processes that have responsibilities for different actors common because they are doing similar things, you may have trouble later on.
This is because even if they seem to be doing the same thing at the moment, as long as the actors they depend on are different, it may be necessary to add different actor-dependent processes in subsequent updates.
In other words, you may end up merging seemingly identical but essentially different processes.
You can distinguish such cases by identifying which actor the process depends on.
Of course, even if the actors are different, the process can be common if there is a relationship between them.
In any case, when practicing DRY, it is necessary to consider the SRP as well and make a careful decision.
Open-Closed Principle (OCP)
The Open-Closed Principle (OCP) was proposed by Bertrand Meyer in 1988.
A software artifact should be open for extension but closed for modification.
(Bertrand Meyer. Object Oriented Software Construction, Prentice Hall, 1988, p. 23.)
It must be open for extensions such as adding features.
"open" means that it can be added without affecting the existing code.
And when modifying the existing code of a module, the effect of the modification must be closed within the module.
The OCP defines these two rules.
What are the situations in which this principle is not being followed?
Even if you want to add a trivial feature, you will need to modify the existing code.
When adding a new feature, we carefully read the dependencies among the thousands or tens of thousands of existing lines of code and make modifications.
The larger the code base, the more man-hours it takes to add features, even if it is a trivial feature.
On the other hand, if you follow OCP, you can limit the impact of specification changes and bug fixes because the modifications are closed to the module.
Being open to extensions and closed to modifications makes the software resistant to feature additions and specification changes.
Liskov Substitution Principle (LSP)
The Liskov Substitution Principle (LSP) was proposed by Barbara Liskov in 1988.
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 such 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.1
(Barbara Liskov, "Data Abstraction and Hierarchy," SIGPLAN Notices 23, 5 (May 1988).)
In other words, "If S is a derivative of T, then it must behave the same even if T is replaced by S.
Since it is difficult to explain with just words, here is an example code.
This can be expressed in TypeScript as follows.
interface T {
value: number;
}
interface S extends T {}
const o1: S = { value: 1 };
const o2: T = { value: 2 };
const P = (arg: T) => arg.value;
P(o2);
P(o1);
Here, for the sake of illustration, we have assumed that T is a simple type with only value. In fact, what details T has has nothing to do with LSP.
S inherits from T.
The function P takes T as an argument.
In this case, since o2 is type T, so P(o2) is accepted.
Similarly, o1, which is type S, P(o1) is also accepted.
In such a case, we say that S is a derived type of T.
In other words, the LSP says that if you want to inherit a type, it should be a derived type.
Looking at this example alone, one might think, "What is this, just inheritance of types, or is LSP just a rephrasing of the language feature of inheritance?
To show that this is wrong, here is an example of inheritance that violates the LSP.
class Rectangle {
set width(w: number) {
this.width = w;
}
set height(h: number) {
this.height = h;
}
calcArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
set width(w: number) {
this.width = w;
this.height = w;
}
set height(h: number) {
this.width = h;
this.height = h;
}
}
const o1 = new Square();
const o2 = new Rectangle();
// Function to find the ratio of the sum of the lengths of the sides to the area
const P = (arg: Rectangle, width, height) => {
arg.width = width;
arg.height = height;
return (width * 2 + height * 2) / arg.calcArea();
};
P(o2, 2, 5); // Can be calculated correctly
P(o1, 2, 5); // 5 * 4 / 5 * 5 is correct, but actually (2 * 2 + 5 * 2) / 5 * 5 is calculated
In this codes, we use the Rectangle type to represent a rectangle and the Square type to represent a square as examples.
This is an example of an old known LSP violation.
Mathematically, according to the definition, a square is a special case of a rectangle, so it might seem natural that Square inherits from Rectangle.
However, a square assumes tighter constraints than a rectangle. It is the constraint that the length of the vertical side is equal to the length of the horizontal side.
Therefore, in Square, when setting the length of the edge, the length of the vertical and horizontal edges are processed to be the same.
As a result, P(o1, 2, 5) is getting a different result than intended.
If there is codes that violates the LSP like this, conditional branching will occur on the client side that uses it (in this case, the function P()).
Then, when there are more classes inheriting from T, it becomes necessary to modify the conditional branch on the client side.
This means that it also violates the Open-Closed Principle (OCP).
Interface Segregation Principle (ISP)
The Interface Segregation Principle (ISP) can be summed up: You should not cram everything into a single interface, but
provide only the functionality that the client needs.
This is also related to SRP.
Consider the following example of an EC site with the following actors.
- Buyer: The user who purchases the product
- Seller: The user who sells the product
Then, these two users are abstracted by a single interface User.
User requests the methods getPurchaseHistory() and getTotalSales(), but the former method is only used by Buyer, while the latter is only required by Seller.
At this time, the implementation of Buyer will need to implement some processing, even though it does not use getTotalSales().
To begin with, Buyer did not need to know about the existence of getTotalSales(), but by violating ISP, it created an unnecessary dependency.
User also violates SRP because it depends on two actors.
Using or modifying User will require changes to both Buyer and Seller, which may also violate OCP.
Being careful not to overburden any one interface according to the ISP will also help to protect other principles such as SRP and OCP.
interface User {
id: string;
name: string;
getPurchaseHistory: () => Purchase[]; // get the purchase history
getTotalSales: () => number; // get total sales
}
class Buyer implements User {
constructor(public id: string, public name: string) { }
getPurchaseHistory() {
const history = …; // Process to retrieve purchase history
return history;
}
getTotalSales() {
return 0;
}
}
class Seller implements User {
constructor(public id: string, public name: string) { }
getPurchaseHistory() {
return [];
}
getTotalSales() {
const sales = …; // Process to calculate total sales
return sales;
}
}
Dependency Inversion Principle (DIP)
The last part of SOLD is Dependency Inversion Principle (DIP).
The Dependency Inversion Principle (DIP) tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.
(Robert C, Martin. "DIP: The Dependency Inversion Principle". Clean Architecture. Pearson Education.)
What is the source code dependencies?
In the case of TypeScript, a dependency is created by referencing import or require.
In other words, the DIP says that when referring to import, you should specify an abstract code such as interface, not a concrete implementation code.
For example, suppose that a.ts imports b.ts.
At this time, a.ts will know the file name of b.ts as well as the classes and methods described in it.
And if b.ts is changed, it will also affect a.ts.
This is even if a.ts is originally a stable code that is not easily changed.
stability of the code
This is where the idea of "stable code" comes into play.
Suppose all the codes could be divided into "stable codes" and "unstable codes".Then, the dependencies can be classified into four types as follows
- stable → stable
- stable → unstable
- unstable → stable
- unstable → unstable
Think of the left side of the arrow as being dependent on the right side.
The first is not a problem because both sides are stable.
In 2, the stable code depends on the unstable code, and the inherent stability is compromised.
3 does not cause problems because the unstable code depends on the stable code.
In 4, the unstable code depends on the unstable code, which makes it more unstable, but it is not as much of a problem as 2.
In other words, we need to eliminate the relationship in 2 and reduce the relationship in 4 as much as possible.
So what exactly are "stable code" and "unstable code"?
In Clean Architecture, we focus on the following two points when considering stability.
- abstract or concrete
- close to input/output or close to domain model
Abstract or concrete?
Hidden in the DIP is the assumption that abstract components are stable and concrete components are unstable.
In order to verify whether this assumption is correct, let's compare the abstract interface with its concrete implementation.
As the interface is modified, the implementation is modified accordingly.
On the other hand, the interface does not necessarily need to be changed when the implementation is changed.
This shows that the abstracted code is more stable.
It can be said that abstraction is the process of extracting the common denominator or essence from multiple concrete objects. Therefore, it is easy to understand that abstracted code is hard to be changed.
However, it is important to note that implementation is not always easy to change.
For example, the standard library of a programming language is an implementation, but it does not change much.
There is no need to be more nervous than necessary about references to such an unchanging implementation.
Thus, abstract or concrete is an important indicator for the stability of a code.
On the other hand, concrete codes are not necessarily changeable.
In addition, please keep in mind that not all codes can be divided into stable or unstable, and that there are varying degrees of stability there.
Close to input/output or close to domain model
As briefly introduced in the first image of this article, Clean Architecture divides the software into layers of concentric circles.
The UI View, DB, storage and other inputs and outputs are located on the outermost side, and the domain model is located on the innermost side.
The intention is to place what we want to stabilize inside the layer and protect it from the unstable outside.
We also use DIP to protect the center of the circle, which we want to stabilize.
This concentric circle architecture is the fundamental idea of Clean Architecture.
This is supported by DIP.
Dependency Inversion
According to the DIP, the destination referred to by import must be stable.
But can we always follow this rule?
There are many cases where you want to call unstable code from stable code for processing.
For example, the Clean Architecture considers DB to be the outermost of the concentric circles and unstable.
If you want to operate DB from a stable code inside the circle, you need to operate an unstable code from a stable code.
As you can see, the flow of processing and the direction of dependencies are usually the same. In other words, if you try to manipulate the DB, you will depend on the DB.
However, this would violate the DIP. We need to reverse the process flow and dependencies somehow.
Dependency Injection (DI) solves this problem.
For more information about DI, please refer to the original book for a detailed explanation with sample codes.
DI reverses the flow of processing and dependencies, and protects the domain model at the center of the concentric circles from instability.
NEXT: Introduction to Clean Architecture for TypeScript: PART3.
Ads
This article is an excerpt from the first chapter of the book "React Clean Architecture". Some wording has been changed. You can buy the kindle version of this book from amazon.
Posted on September 12, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.