Clean architecture from a purist perspective without violating any boundaries.
1. Entities
- Domain entities
2. UseCases
- Contracts (Interfaces) for data-access, security and web
- Implementation Use Cases
3. Interfaces
- Factories for Use Cases, data-access, security and controllers
- 'duck-type' interfaces for the UseCases*
- Contracts (interfaces) for presenters and viewmodels
- Implementation data-access, security and controllers
- Mappers
4. Plugins
- Factories for routers
- Contracts (interfaces) for routers
- Implementations Routers
- MiddleWare
Utils
- Contracts (interfaces) for Exceptions
- Helper classes
- Environment
*These UseCase interfaces serve only to help the UseCaseFactory with inferring the right presenter, this interface will not be used by the use-case layer.
The biggest challenge was designing the presenters without violating the boundaries. Inheritance is used to append the present function to the UseCaseOutput.
The UseCase will return an object that looks a bit like this:
(output: UseCaseOutput) => {/* nothing */}
That is why the UseCaseFactory uses a generic inference to defer the implementation of the viewmodel to the outer layer.
export type IUseCaseFactory = <Tviewmodel> (
/* services */
presenter: IPresenter<IUseCaseOutput, Tviewmodel>,
) => IUseCase<IUseCaseInput, Tviewmodel>
The controller factory will use the specific implementation of the presenter to add the present function to the UseCase without having to use it, the controller will not return the ViewModel but the complete presenter
// Contracts
export type IViewModel = IViewModelResponse<{
/* Output */
}>;
export type IPresenter = IPresenter<IUseCaseOutput,ISignUpViewModel>
// Factories
export const presenterFactory = {
presenter: (): IPresenter => presenterImpl
}
const xyzUseCaseFactory = (): IUseCase => {
return resolveUseCase(
/* Injected services */
presenterFactory.presenter()
);
}
This way the implementation stays hidden from the usecase but the use case is able to return a partial callback.