Clean Architecture in Nutshell

Clean Architecture in Nutshell

This article is a summary notes of "Clean Architecture" book. If you haven't read it, this article is a must, though you should read the book, if you can. If you have read it already, this is a quick refresher.

This is a long read so bookmark it and start once you have 15-30 mins.

What's an architecture?

Architecture is art of drawling lines !! On a serious notes, does a definition matters? Yes its, it gives you a guidance of what you are supposed to do when you are going to build a system. Out of all definitions flowing around, The one i like is following

"Architecture is making decisions early in the software life-cycle which helps in deferring as many decisions as possible and keep as many options as possible, for as long as possible."

Yes, Architecture is essentially keeping your options open. But before we talk more about why that's the case and how to achieve that in a software. Let's take a step back and talk about "What is software?". A software is essentially a piece of code to perform a operation. This can also be referred as behavior part of the software. But there is also another essential part of every(should be) software which is its structure. This is the part which architecture rules focus on.

The only reason we care about the structure of software is that we want to keep our options open. If we know for certainty that we have written a software once and we will never touch it again, the only thing to care is behavior. As far as there is no bug and it works the way its supposed to, we don't care how exactly its structured. But there is no such software so structure (hence architecture) is a essential piece.

To understand whats architecture let's think of a scenario where there is no architecture? We can write whatever software we write with only one rule that it should work. This is great flexibility. You can write your code in whatever classes you want, you can create as many services as you want. So essentially architecture rules are those restrictions that we put in place to promise a longer lifecycle for software.

Design Principles

No alt text provided for this image

Lets talk about the architecture rules that keep our options open and increase life of our software. These principles are SOLID (literally !!)



Single Responsibility Principle(SRP) tells you that a module, which can be one class, a java package or a service, should have one reason to change.

This is a ambiguous definition and results in multiple interpretation. This is generally interpreted as "A class should have just one functionality". Uncle bob has emphasis this point in his book that this interpretation is incorrect. A class is not meant to have one functionality. A public method is one functionality and class is group of functionality. So he has given another definition "A module should have one actor responsible for the change".

Lets understand with a example.

Class Employee {

    public Salary getSalary() {...}

    public Location getCurrentSeatingLocation() {...}
}

Below code has 2 functions which will change for different reasons. The actor which will govern the change for location(physical seating) is completely different from its salary(payment systems).

On the other hand below two functions make perfect sense in one class (though class is doing multiple things)

Class Employee {

    public Salary getAnnualSalary() {...}

    public Salary getHourlyWage() {...}

    public Salary setHourlyWage() {...}
    
 }
        

In traditional sense SRP principle is used in context of classes. At the level of components, it called the Common Closure Principle.

Open Closed principle(OCP) tells you that A software artifact should be open for extension but closed for modification.

Its intuitive to understand the advantage of this principle as every coder knows changing a existing code is always more buggy and painful that writing a new one. But we don't want to write the whole software again. So Lets talk about how we can apply OCP.

Lets talk about a use case where we want to provides API to write and fetch salary of a employee. In a very simple version this will include 3 layers, a Activity layer which takes the API request, validate the request(on syntax etc), convert it into business models and call BO layer. a BO layer that will do business specific validation like salary is within specified range and convert this to a repository object and call repository layer. A repository layer which will put the salary in database. Below is a class flow diagram.

No alt text provided for this image

Ok, may be you guessed where its going. I have not used any interface in above class diagram. Before we do that, lets admire what above flow does correctly.

  • Layered, We have layered architecture. Each class will have one reason to change. we are already following SRP.
  • Abstraction, GetSalary activity talks to Salary provider and doesn't care about repository. Any change in repository have no impact on activity.

But what we are not doing correctly.

  • Coding on Concrete classes, We are coding against concrete classes. We need interfaces so that changes in SalaryProvider implementation has no impact on GetSalaryActivity.
Figure 1.1

OK, so we have interface and implementations and we can replace implementations with another implementation. We have started following OCP. But a major point of emphasis is direction of arrows in above diagram. These direction of arrows are referring to source code dependency. So SalaryProviderImpl source code depend on the SalaryProvider but not the other way arround. Or in other terms following 2 should happen

  1. Changes in SalaryProviderImpl should not compile GetSalaryActivity.
  2. GetSalaryActivity should not have any transitive dependency on SalaryProviderImpl. Note we are not talking about that coder should not access SalaryProviderImpl from GetSalaryActivity but he should have no way to do it systematically.

2nd rule above actually makes a plug and play architecture in real sense. A ideal way to achieve it that SalaryProviderImpl is maintained in a seperate source code package. You can replace the SalaryProviderImpl, which is in pkg A, with SalaryProviderImprovedImpl, which is in pkg B by replacing A.jar with B.jar

Why having even transitive dependency is bad if we follow best practices? Its because we are breaking the principle that a software should not have access to anything it doesn't need. We have seen all those imports coming from a transitive dependency and making our life hell !! (Java 9 modules is also going to solve this by limited access of what to expose from a module)

Liskov Substitution Principle(LSP), tells that super classes should be replaceable by sub-classes.

This principle is essentially talking about inheritance. We will not go in more detail on this one as this is intuitive with knowledge of inheritance.

Interface Segregation Principle(ISP), tells you that class should not be forced to implement a method it doesn't need to. In other words, a module shouldn't depend on inherit from a module which has more than what it need.

This can be practices by essentially breaking your implementation in multiple interfaces in class context and breaking your modules/components in multiple modules in high level architecture context.

Dependency Inversion Principle(DIP), tells you that source code should refer to abstraction and not on concrete implementation.

This is software 101 to use interface as they are less volatile than implementation so you save your client with changes of implementation. But why this is classed dependency "inversion" principle? Because to achieve this the source code dependency is inverted from flow of control. Lets look at diagram of our SalaryActivity again below. The green pointers are the flow of control which is opposite of source code dependency.

No alt text provided for this image

Component Design Principles

SOLID principles are primarily for class level but in essence they apply to component level also. Uncle Bob has explicitly talked about component level design principles and grouped them in different category. For the same reason you feels we are repeating ourselves when we talk about some of these component level principles.

First category is component cohesion and have 3 principles

REP: The Reuse/Release Equivalence Principle

The principles states that Classes and modules that are grouped together into a component should be releasable together.

CCP: The Common Closure Principle

Gather into components those classes that change for the same 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 restated for components.

CRP: The Common Reuse Principle

Don’t force users of a component to depend on things they don’t need. Or in other words don't create components which has unrelated things and client will get unnecessary dependencies. For example, don't create a package like MyAllDependencies. A new client who only need 1 dependency will get all other unnecessarily. The CRP is component version of the ISP(Interface segregation principle).

Second category is component coupling

Acyclic dependency principle, Allow no cycles in the component dependency graph.

Cyclic dependencies creates coupling. for example, if A->B->C->A, the independent development and compilation is not possible.

Stable dependency Principle, Depend in the direction of stability.

Less stable component should depend on more stable component not the other way around. Though question is how you define stability. One important definition of stability is component which need less changes is more stable. This is why we code against interface which is more stable than implementation.

Another measure of stability of how many dependencies you have. If you no outgoing dependency you are most stable. "Dependency inversion principle" plays a crucial role in practicing this principle as we inject the unstable components than depending on them in source code. (Note unstable doesn't mean buggy but sure has more chance to have bug as it will have more touches and more dependencies)

Stable Abstraction Principle, A component should be as abstract as it is stable.

The Stable Abstractions Principle (SAP) sets up a relationship between stability and abstractness. On the one hand, it says that a stable component should also be abstract so that its stability does not prevent it from being extended. On the other hand, it says that an unstable component should be concrete since its instability allows the concrete code within it to be easily changed.

What's Architecture(Again)?

We have already given the definition but lets talk what are the attributes a architect should focus on

Development, A software system that is hard to develop is not likely to have a long and healthy lifetime. So the architecture of a system should make that system easy to develop, for the team(s) who develop it.

Deployment, To be effective, a software system must be deployable.

Operation, The architecture of the system should elevate the use cases, the features, and the required behaviors of the system to first-class entities that are visible landmarks for the developers.

Maintenance, By separating the system into components, and isolating those components through stable interfaces, it is possible to illuminate the pathways for future features and greatly reduce the risk of inadvertent breakage.

Keep Options Open, The goal of the architect is to create a shape for the system that recognizes policy as the most essential element of the system while making the details irrelevant to that policy. This one is the most important so lets talk in more detail

For example:

  • It is not necessary to choose a database system in the early days of development, because the high-level policy should not care which kind of database will be used.
  • It is not necessary to choose a web server early in development, because the high-level policy should not know that it is being delivered over the web.
  • It is not necessary to adopt REST early in development, because the high-level policy should be agnostic about the interface to the outside world.
  • It is not necessary to adopt a dependency injection framework early in development, because the high-level policy should not care how dependencies are resolved. (For example, you shouldn't use Guice Inject or Spring Autowire in your business code, use the generic Inject)

A good architect maximizes the number of decisions not made.

Decoupling Modes

So we know decoupling is important to keep the architecture flexible and keep options open. Lets look at decoupling modes

Source code level, We can control the dependencies between source code modules so that changes to one module do not force changes or recompilation of others.

Service level, We can reduce the dependencies down to the level of data structures, and communicate solely through network packets such that every execution unit is entirely independent of source and binary changes to others (e.g., services or micro-services).

So which one to use? One solution (which seems to be popular at the moment) is to simply decouple at the service level by default. A problem with this approach is that it is expensive and encourages coarse-grained decoupling. Ideal is push the decoupling to the point where a service could be formed. should it become necessary;

A good architecture will allow a system to be born as a monolith, deployed in a single file, but then to grow into a set of independently deployable units, and then all the way to independent services and/or micro-services.

Business Rules

As we can see a software revolved around the business rules We want to keep the business rules stable and at core while things like DB, UI are peripheral.

But whats business rule? Is your core also business rule, what about validation done before the rules?

As a rule of thumb, a critical business rule is something which we have to do even if we don't use computer. Like the core interest calculation. The data on which these business rule run are the entities. Now the validation that was done as part of request is not critical business rule as its done because we are sending data over wire.

So everything should depend on core business rules and entities and they shouldn't depend on anything as they are the most stable part. The core rule will have no knowledge of DB as DB is a computer system concept. Uncle Bob has defined these non core business logic as Use cases.

So in other words, we have use cases which depend on core business rules/data. Use case in that sense orchestrates the whole flow.

Screaming Architecture

Uncle bob has done a interesting comparison of computer architecture with a building architecture. When you look at building architecture, it tells you its a hotel or home or hospital. Similarily your architecture should scream at first look that its a payment system vs retail system and not spring, guice and SQL.

In other words your architecture should have minimal coupling with framework. From high level architecture to lowest level coding think of what will happen if frameowork is replaced. Will your core code still work and portable?

Clean Architecture

So how a good system look like?

  1. Independent of frameworks.
  2. Testable.
  3. Independent of the UI.
  4. Independent of the database.
  5. Independent of any external agency.

Another practice that's followed in keeping different layers independent is Interface Adapters. These adapters converts object from one layer to another layer to keep them independent and not coupled. For example, a use case specific DO or a repository DO should have a conversion logic(interface adapter) to emit business object. Note business object is not having a method which takes use case DO and produce BO as BO layer is core and shouldn't be aware of use case. Or in other words, when we pass data across a boundary, it is always in the form that is most convenient for the inner circle.

Lets also talk about how architecture should focus on test-ability. For example, testing UI is hard, in cases like these architect should break the hard to test into 2 layers. The hard to test UI layer will be very thin(only rendering) and there will be another testable layer which will be consumed by this hard-to-test layer.

For example, a UI layer will be broken into presentable and view layer. Presentable layer will return all the attributes and is a testable API, view layer will only render it. Uncle bob has called this humble object pattern where view is humble object with no logic and only basic transformation.

Services

Any discussion on architecture is incomplete if we don't talk about services explicitly. Services are considered by default layers in architecture.

While considering services as the boundary of layers we should be clear that only difference between a service and source code package is that services are physically seperated and independently deployable.

In other words, architectural boundaries do not fall between services. Rather, those boundaries run through the services, dividing them into components. To deal with the cross-cutting concerns that all significant systems face, services must be designed with internal component architectures that follow the Dependency Rule. Those services do not define the architectural boundaries of the system; instead, the components within the services do.

Keep building, keep coding...





To view or add a comment, sign in

Others also viewed

Explore content categories