[Book Study] Clean Architecture — Part 2

Zong Yuan
5 min readOct 10, 2021

This article covers the design principle(SOLID) in the book and component principle will be covered at next article.

First how SOLID principle and component principle related to software architecture? Design principle is principle for software architects to follow when designing architecture at module level. And component principle is principle for architecture design at software component level.

Like designing for a new feature, SOLID principle is good reference. While design a full service (Like a new service in microservices), or start a new software that included different components/services, component principle will be a good reference.

Design Principle

Design principles tell us how to arrange our functions and data structures, into classes, and how these classes should be interconnected

The “classes” here does not represent the Class in object-oriented programming, it is a coupled grouping of functions and data (Which in OOP might be a Class).

The goals of design principles are aiming to create a software structures that:

  1. Tolerate change
  2. Easy to understand
  3. Basis of components can be used in other software systems

The design principles here refer to SOLID principle:

  • SRP: The Single Responsibility Principle
  • OCP: The Open-Closed Principle
  • LSP: The Liskov Substitution Principle
  • ISP: The Interface Segregation Principle
  • DIP: The Dependency Inversion Principle

SRP: The Single Responsibility Principle

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

There are multiple benefits of SRP, first is reduce the risk of side effects when making changes. Secondly, the occurrences of merge conflicts can be minimized at the same.

Doing unit testing will be easier with SRP since you only testing for single actor requirements.

Example

I try to provide an example that different with the book.

Imagine your system provide a new promotion feature which included 2 different promotion types. Both promotion types are sharing same basic calculation for the bonus amount, but let’s say one of the promotion types required extra calculation, then the code look like below.

public BigDecimal calculate(promotionType type) {
BigDecimal amount = basicCalculate();
if (type == PromotionType.A) {
// Extra calculation for type A
...
}
return amount;
}

So, what is the problems with code above?

Imagine PromotionType.A and PromotionType.B have two different group of stakeholders (actor), if one of them requests to modify the basic calculation, it will affect the other promotion type since both of them sharing the same calculation module. Also if the number of promotion number increase in the future, then your code will have a lot if-else or switch in this function.

You can consider to create a BasicPromotionCalculator which implemented the basic calculation, then create PromotionACalculator which extends from BasicPromotionCalculator and customized the calculation. Remember to use an interface so you can always invoke different implementations of calculation with the interface (which also follow the OCP).

public interface PromotionCalculator {
BigDecimal calculate();
}
public class BasicPromotionCalculator implements PromotionCalculator {
public BigDecimal calculate() {
...
// Basic calculation
}
}
public class PromotionACalculator extends BasicPromotionCalculator {
@Override
public BigDecimal calculate() {
BigDecimal amount = super.calculate();
... // Extra calculation for type A
}
}
...public BigDecimal calculate(PromotionCalculator calculator) {
return calculator.calculate();
}

Maybe there are better ways to achieve the SRP in this example, please feel free to correct me.

OCP: The Open-Closed Principle

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

In other words, design your software architecture to make your software easier to extends/adds new feature without modifying your original architecture.

The modification of architecture means more efforts and costs required for the changes. At the same time, there will be more changes in your code, which causes the possibility of bugs/issues occur increased.

Example

Let’s go back to the promotion example in SRP section.

public void apply(Member member, PromotionType type) {
...
// Apply promotion on member
// After applied the promotion, each type of promotion need to do some post handling
switch (type) {
case A:
// Do something for A
break;
case B:
// Do something for B
break;
}
}

Now if we need to add a new promotion type C into the system, then we can’t avoid to modify to original apply function to add in case C: in the switch cases.

A solution for this case is pass in the post handler by using lambda and @FunctionalInterface .

@FunctionalInterface
public interface PromotionPostHandler {
void postHandling();
}
public void apply(Member member, PromotionPostHandler postHandler) {
... // Apply promotion
postHandler.postHandling();
}
public void applyPromotionA(Member member) {
apply(member, () -> {
... // Do something for A
});
}

Therefore, the apply function is closed for modification but opened for new promotion type in the future.

LSD: The Liskov Substitution Principle

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 behaviour of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

In short, make sure your subtype (child class) do not violate the defined-behavior or interfaces, because your system will always expect same behavior even the original type (parent object) is substituted by subtype (child object).

Sorry can’t think of any good example for LSD.

ISP: The Interface Segregation Principle

Prevent class implement unused interfaces.

The best examples are segregation of file operation interfaces in Java library. There are different interfaces such as Closeable, AutoCloseable, Writable, Readable provided in the Java libraries, which each of them represent single type of operation.

We need to take ISP into consideration when decide to make our software depends on a framework. If the framework is depends on another database, it mean our software now is depends on the database too.

Example

Take the example in SRP and OCP, let’s say now we decided to add a function that will generate a formula explanation for promotion calculator, but for promotion type A only (I know it is not a good example, but please bear with it first). Since promotion type B is not required to implement this function, so we should segregate it from PromotionCalculator. In shorts, we should create a new interface for this function, and only PromotionACalculator will implements it.

public interface PromotionExplainer {
String explain();
}
public PromotionACalculator extends BasicPromotionCalculator implements PromotionExplainer {
...
}

DIP: The Dependency Inversion Principle

Source code dependencies should refer only to abstractions or non-volatile concretions.

With helps of interfaces or other dependency inversion mechanism, we can always control our flow of dependencies. Therefore, we should always control our source code to depend only on interface/abstraction or non-volatile concretion to prevent the volatile part of the dependencies.

The book provides some practices on this principle:

  1. Don’t refer to volatile concrete class
  2. Don’t derive from volatile concrete class
  3. Don’t override concrete functions
  4. Never mention the name of anything concrete and volatile

Spring framework provides dependency inversion via their Bean and proxy mechanism.

Factory design pattern is a good way to help you separate the creation of concrete implementation and the abstract interfaces. Although using factory pattern do not fully avoid DIP violations, but we can gather these violations together into a small portion of code.

Conclusion

As we can see from above 5 principles, all of them are trying to control your software architecture to meet following goals:

  • Reduce changes affection in future. Extension/New CR of software won’t affect the original functions, thus can reduce the development cost and also the risk of causing new issues on old functions.
  • Less conflicts of interest. Since ideally every developer/stakeholder will focus on different part of code, the cost to solve conflicts will be lesser.
  • Easier to maintain the software. Since every parts of software have their own dedicated actor/structure/implementation, the efforts to maintain the software will be minimized.

Deployment cost can be reduced for new CR or bug fixes if your software are constituted by multiple deployable services/components.

--

--