With the constant evolution in the software development process and the growing adoption of different frameworks, is becoming very common developers getting comfortable with the structures provided by these tools and leaving aside some principles of good software development. We need to keep in mind that these tools are just the path and not the end. Our software needs to be more “Domain Oriented Software” and less “Framework Oriented Software”.
That’s not a criticism with the frameworks and libraries adoptions, they really need to be used, there are a lot of wonderful projects that are here to help us, but we should use them in a way that these tools are adapted to our solution, and not the opposed.
Our domain does not need to know what web framework or what database system we are using, these things are just plugins that we may define later.
Thinking about this problem, many solutions have been created and one of these is the “Clean Architecture”, presented by Uncle Bob.
That post will give you a little introduction about Clean Architecture, your main concepts, and a way to implement it, giving us an example of an application build with ReactJS.
The main purpose of the Clean Architecture is the Dependency Rule, this rule is all about the direction that our dependencies should point to, that is, always to the high-level policies.
The high-level policies are defined as the core of our application, the components that are independent of any programming language or technology, the policies that only need to change when our domain changes, that is, only in very specific cases.
In contrast, the less specific the component is, the lower the level will be. Here we may put the repositories that connect to our database, the HTTP client that made requests, the presentation layer responsible for the UI, and some components that need to talk with third party libraries for example.
We can see that the low-level policies are responsible for things that aren’t specific to our domain but specific for our application, and the application is just the way we choose to solve our domain problem.
The figure below exemplifies how the Dependency Rule works.
As you may see, all the externals agents point in the same direction, and that pattern may give some benefits to us.
Our entities and use cases don’t have any external world dependency, the only concern that they have is about the domain itself.
So, if we need to change any external agent implementation like an HTTP client, we don’t need to change anything in our use cases, just in the HTTP client concrete class implementation.
That’s another crucial point in the Clean Architecture, all cross-layer communication is made through solid interfaces. Dependency Inversion has a crucial role in that design, as a matter of fact, we only can get the most of Clean Architecture with we know how to implement all the SOLID principles correctly.
Let’s say we have a use case responsible to send a picture to the server. To accomplish that use case, we decided to build a front-end client application with React + Typescript.
We also decided to follow some implementation of the Clean Architecture structure proposal, so, our application will have the following layers:
Domain Layer: The Domain Layer is the most high-level policy that we have, is where we define the entities and the use cases. We don’t have any concrete class in that layer, only interfaces. All the use case implementations will be in the next layer, the Data.
Data Layer: Right behind the Domain Layer, this layer is responsible to implement all the use cases and to define protocols (interfaces) that use case needs. The protocols defined in that layer will be implemented on the next, the Infra.
Infra Layer: That Layer will implement the protocols defined in the Data, normally, those implementations are designed to the external world communications, such as database operations, HTTP requests, third-party libraries, and so on. If we think of a client application, like a React app, in that layer we have the HTTP clients, for example.
Looking with more attention to the last figure, we can see that the Infra Layer belongs to the outer circle, in that circle we do have concrete implementations of libraries and frameworks, it is the most low-level policy that we have, those that aren’t a domain-specific problem.
In that circle, we also have the Presentation Layer, responsible for the UI. Here, in our example, that layer will implement all the React code.
Separating the things in that way, we can see that our core code is completely independent of the library that we choose. We only see the React in one layer, only making what it is designed to do, leaving the business code to other layers. Isolating the app like that, we have many more benefits than coupling all the domain code in our UI.
Following our example, first, we need a use case responsible to send images to a server. Let’s call him SendImage.
As you can see, our use case is just an interface that defines a method send and returns a Promise of a DetectedImage (a specific entity to our domain).
Now that we have our use case, we need an implementation for him, let’s create a RemoteSendImage in our Data Layer.
Some important things to notice about that implementation:
In the class constructor, via dependency injection, we are defining that this class needs an implementation of a HttpPostClient, and a URL.
Those dependencies are specific to that implementation. If we have another implementation that does not send the image over HTTP, the dependencies will be another.
Now, that we defined that we need a HttpPostClient protocol, let’s create in the Data Layer too.
Just like our use case, that protocol is just an interface that defines a post method. For that implementation, the axios library was chosen, now we create an AxiosHttpClient concrete class that implements the HttpPostClient.
An important thing to notice about that implementation is his interface. Why we didn’t create a more generic interface, instead of one that only knows POST request (HttpPostClient)?
At that moment, we only need to make POST requests, our use case implementation doesn’t tell not about any GET or PUT requests. So, following the Interface Segregation Principle, we make an extremely cohesive interface, that only knows the method that we need at the moment. If in the future we need to make a GET request for example, we define a protocol for that and make our AxiosHttpClient implement that interface too.
Now it’s the time that React comes in. In our Presentation Layer, we will define a functional component that receives a SendImage use case via dependency inversion in their props.
There are some important things to notice here:
A React functional component that only used hooks for your internal logic;
The fact that the component receives an implementation of SendImage in his props is one of the things that make the app structure more flexible;
The first advantage of this structure is that our UI component doesn’t know anything about sending an image to the server, he only needs someone that knows how to the that. Making your only concern about the UI. That’s the Single Responsibility Principle.
Another advantage is that if in the future we decide to send the images over another protocol, we only change the use case implementation, the UI will not even notice that change. With the help of the polymorphism, we can change the implementation easily (Liskov Substitution).
In the end, our folder structure will be like:
Unlike most of the examples that we can find on the internet, this implementation provides a very different approach, where the focus of our UI is only the presentation logic. We were able to create a component completely independent of our domain logic and vice versa.
If we have a use case that needs to send an image to the server, it doesn’t matter if we use a page created in React, Angular, or Vue, our domain is not concerned with that, it is only concerned with the core of the application. Our domain needs to work in the same way regardless of the framework or libraries we are using. If a more delicate exchange is ever needed, we can do it without having to invest a lot of time in it.
From the moment that we have a more flexible and robust architecture, in addition to being strongly abstracted, we are able to observe in practice the benefits of object orientation and its principles being fulfilled. At first, it may seem too much work, and it is indeed, because it is necessary to create many protocols and implementations, however, in the long run, it is a trade-off that is really worthwhile.