Closed zybzzc closed 1 year ago
Hey there!
In short, no, the application layer should not depend on the implementation of services (adapters). This can be achieved via dependency injection. In the file you're referring to, hooks are used only as a “DI” mechanism to “associate” a specific port with the specific implementation.
In detail, let's break down the idea behind dependencies and their injection first.
If I understand correctly, the second piece of code will be placed in a file somewhere in the services or adapters folder, which if that is the case does meet the requirement that the application layer does not depend on the adapter layer.
Not quite. In a good way there shouldn't be such code at all, and the DI (dependency injection) should be automatic or at least not require too many actions to register a service.
In a perfect world the code from the example would look like this:
// Depending the domain, OK:
import { UserName } from "../domain/user";
// Depending on the ports.
// They're the part of the application layer, OK:
import { AuthenticationService, UserStorageService } from "./ports";
export function useAuthenticate(
// Declaring what ports are going to be used in this use case.
// This can be read as “what pieces of behaviour this use case needs to fulfil its task“:
{ storage, auth }: {storage: UserStorageService, auth: AuthenticationService}
) {
async function authenticate(name: UserName, email: Email): Promise<void> {
const user = await auth.auth(name, email);
storage.updateUser(user);
}
return { authenticate };
}
// Notice that there's no mention of any “services” or “adapters”
// because on the interface level there are no dependencies on them.
// On the contrary, the application layer _specifies_ the required behaviour,
// it's the adapters who _“follow”_ and must implement it.
An adapter would need to specify what a port it implements:
import { AuthenticationService } from "../application/ports";
// “Associate” the `AuthenticationService` with the specific `auth` implementation
// so the DI later injects `auth` in every place where `AuthenticationService` is required:
export const auth: AuthenticationService = {
auth() { /*...*/ }
}
With magical automatic DI, that'd be all. No dependencies, no manual registration, “it just works”.
If we then wanted to replace this adapter with another, we'd need only to bind the AuthenticationService
interface with a different adapter:
export const anotherImplementation: AuthenticationService = { /*...*/ }
Unfortunately though, we don't have such magic automatic DI, so we need extra steps to set up such an association between interfaces and implementations. For example, we might need to “register” the services somewhere using a DI tool:
import { container } from "../infrastructure";
import { AuthenticationService } from "../application/ports";
const auth: AuthenticationService = {
auth() { /*...*/ }
}
// Manually telling the DI container to make an association
// between the `AuthenticationService` interface
// and the `auth` implementation:
container.register<AuthenticationService>(auth);
There are many different tools for it, I won't go into the details of every one, I'll just leave a link to one example. But the point is that if we can't rely on automatic DI by default without setting up the infrastructure, we need to somehow do it manually.
So, this snippet you pointed out:
function useOrderProducts() {
const notifier = useNotifier();
const payment = usePayment();
const orderStorage = useOrdersStorage();
return (user: User, cart: Cart) =>
orderProducts(user, cart, {
notifier,
payment,
orderStorage,
});
}
...is actually manual version of DI. It basically takes the specific implementations (useNotifier
, usePayment
, useOrdersStorage
) and associates them with the required Dependencies
type in the orderProducts
. It's kinda “infrastructure” code that serves as a tool for DI.
The orderProducts
still doesn't “depend” on the services implementation details though. It assumes that anything with the type of NotificationService
follows the behaviour specified in that port and uses this assumption to work with it.
And here we come to the second part of the question:
The problem is that almost all use cases will have dependencies, which leads to the fact that all use cases under application will have an adapter in the adapter layer just to inject dependencies for that use case.
If we think of ports as of dependencies, then yes, the application port depends on them because they formulate what functionality is required for the use case to do a task. But they are a part of the application layer. It declares them as a contract on behaviour it needs to fulfil the task so we can technically say the it “depends” on those.
However, in the post, by dependencies we mean dependencies on the specific entities: on the services' implementations, on the specific adapters, etc. We don't want these because they increase coupling between the parts of the system.
The DI can help with distinguishing in the code between “conceptual dependencies” (ports) and “implementation dependencies” (adapters and services). With it, it's easier to see the direction of implementation dependencies and notice that the application layer doesn't rely on the services or adapters per se but instead only on the ports (its own part).
In the post, I didn't pay much attention to the idea of DI just because it would make the post even longer. But I hope this comment makes the idea of dependencies clearer!
Thank you for such a detailed answer!
So, only orderProducts
belongs to the application layer of our design, and useOrderProducts
and the specific instance code to get the dependencies in it belong to some kind of architectural (DI) code.
Happy to help :–)
https://github.com/bespoyasov/frontend-clean-architecture/blob/ac2f4fc60fa7b7b06eb80761c46202969d881376/src/application/authenticate.ts#L2-L3
The above application layer authenticate code depends on the adapter layer's
authAdatper
andstorageAdapter
.I noticed that you mentioned this place in your blog post:
If I understand correctly, the second piece of code will be placed in a file somewhere in the services or adapters folder, which if that is the case does meet the requirement that the application layer does not depend on the adapter layer.
The problem is that almost all use cases will have dependencies, which leads to the fact that all use cases under application will have an adapter in the adapter layer just to inject dependencies for that use case.