Pergunta

Recently, I've been studying Clean Architecture and I have some doubts. I want to make a REST API that adheres to this architecture. To do that, I define my entities, use-cases, etc. For each endpoint, I use a different controller that calls for a use-case. My problem is that for all examples of controllers that I have seen, they use the request object of their respective framework as argument for the controller. For instance, the Request object of ExpressJS.

From my understanding, controllers should be framework independent. But I believe they aren't because they accept an object from a framework.

To solve this, I believe I should use a mapper that maps the request object to a generic request object accepted by the controller.

Is this the correct approach?

Another question: is the controller what should be called in the REST API? I believe the controller calls for a use-case and the use-case calls a presenter, but I'm unsure.

Foi útil?

Solução

TLDR Version:

Don't introduce additional complexity with a mapper. At the very edge of the application, something has to depend on the framework. You controller can take the request object, convert it to whatever data structure the use-case needs, and pass it along. As the software evolves, if the logic in the controller ever becomes more complicated (e.g., the controller needs to do more work after conversion, but before calling the use-case), you can extract that into a separate object that now represents your controller, and what was left behind (the original controller) is now a gateway to the framework, or perhaps just some glue code.

Full Answer:

Think of the Clean Architecture as more of an ideal to strive to, rather then something you need to setup exactly right from the beginning. It sort of gives you a mental framework for thinking about how to structure your software and control the coupling as the codebase evolves.

At the very beginning, you don't have enough information to make all the right decisions. You can make some generalized educated guesses, or some problem-specific assumptions if you're particularly familiar with the problem domain, but there are going to be things that are architecturally relevant, that you're only going to learn over time, as you work on your code and as you release and get feedback - your understanding of the problem (and how it should be represented in code) will improve, you'll start to figure out what the different axes of change are (SRP), what aspect are stable/volatile, what parts of your codebase change most often, etc.

This is why Agile is against the "Big Design Up Front" philosophy, and favors instead some (reasonable amount) of design at the start, followed by a bunch of short iterations where you get to redesign aspect of your codebase in response to what you've learned.

The problem is, if you design too much too early, you'll come up with a bunch of abstractions that are wrong for what your application actually needs to do, and you won't know it until later when they get in your way. Abstractions1 are hard to change because they encode our assumptions, and everything else depends on them. This is also why we have heuristics like YAGNI, and the Rule of Three - both of these essentially tell you to wait a bit longer to see if your assumptions check out.


1 Abstractions are often interfaces and abstract classes, but they don't have to be - they can be concrete classes, data structures, mechanisms that rely on a naming convention, etc. An abstraction is just something that captures certain key aspects of some thing, allowing those that use that abstraction to not worry about the details that are irrelevant. A wrong abstraction is one that misidentifies which aspects are essential vs which are irrelevant in a given context.


So with that in mind, don't try to solve all the problems at once, and don't expect that you'll never have to redesign anything; the idea is to steer the design towards something that's more stable (based around a set of abstractions that don't change often) and something that better serves the needs of your application (including making certain aspects of it easier to change).

To solve this, I believe I should use a mapper that maps the request object to a generic request object accepted by the controller.

Don't introduce additional complexity with a mapper - at least not until you know you really need that. And most certainly don't do it just for the sake of not depending on the framework. At the very edge of the application, something has to depend on the external frameworks/libraries (you can preserve the dependency rule by considering that external dependency to be an internal detail of that layer, but that's not so important).

For now, you controller can take the request object, convert it to whatever data structure the use-case needs, and pass it along. This is your current dependency structure (your UseCase might have input and output ports, but I'm ignoring that in this image):

Initial dependency structure
(Here the dashed arrow represents a generalized dependency - it's not specifying how one object depends on another, just that it does.)

As the software evolves, if the logic in the controller ever becomes more complicated, you can extract that piece of code into a separate object that now represents your controller (or, the core logic of your controller). What was left behind (the original controller) is now a gateway to the framework, or perhaps just some glue code. E.g., perhaps the controller needs to do more work after conversion, but before calling the use-case, or whatever.

So you may end up with something like this (note that this is only one possibility; in your particular case, the graph may look a bit different - e.g., you may have your controller split into two collaborating objects, one of those objects may implement an interface required by the use case, etc.)

Core controller logic extracted
Core controller logic extracted; in your application, the structure could be different.

Now, while it's possible that you'll replace the framework at some point, most of the time that's not very likely; however, there are other benefits when you perform this extraction. Remember, the context is that you noticed that the logic of your controller is becoming somewhat complicated, so extracting this core logic and rewriting it in a framework-independent way helps you to express that code in a more succinct and readable way, without all the clutter required to juggle the framework (you push that responsibility to the glue code). Better readability means that the code is understandable and that the logic is easier to follow and check - so that's a plus for maintainability. And because the controller now relies on the IRemoteClient interface (you may be able to come up with a name that better suits your application), you can write tests for that logic, etc.

Licenciado em: CC-BY-SA com atribuição
scroll top