I am trying to understand how should I implement a composition root in a project.
From what I have red, if use the composition root the wrong way (for example, by referencing it in lots of place in your application code), you will end up with the service locator.
Let me show you an example of a projcet with out a compositon root.
I have the following project structure:
- server.ts
- domain.ts
- application.ts
- api.ts
- sql-repository
server.ts:
This file imports the API and initializes the server.
import express from 'express';
import API from './api'
const app = express();
const port = 3000;
app.use(express.json());
app.use(API);
// Start server
app.listen(port, () => {
console.log('listening on port: ' + port);
});
domain.ts:
This file holds the core logic of the domain.
export type Entity = {
param1: string,
param2: string,
};
export type IRepository = {
GetMultipleEntities(filterParam: string): Entity[] | undefined
GetEntity(filterParam: string): Entity | undefined
CreateEntity(entity: Entity): void
UpdateEntity(entity: Entity): void
}
application.ts:
This file holds the use cases of the application.
import {IRepository} from './domain';
export const CheckIfEntityExists = (filterParam: string, entityRepository: IRepository): boolean => {
let entity = entityRepository.GetEntity(filterParam);
return typeof entity != "undefined";
};
sql-repository.ts:
This file holds the concrete implementation of the IRepository interface
import {Entity, IRepository} from './domain';
export class SqlRepository implements IRepository {
GetEntity(filterParam: string): Entity {
//
// some sort of logic to get entity from an sql database
//
return {
param1: '',
param2: ''
};
}
GetMultipleEntities(filterParam: string): Entity[] {
//
// some sort of logic to get multiple entity from an sql database
//
return [
{
param1: '',
param2: ''
},
{
param1: '',
param2: ''
}
];
}
CreateEntity(entity: Entity): void {
// some logic to enter new data to the sql database that represents an entity
}
UpdateEntity(entity: Entity): void {
// some logic to update the entity
}
}
api.ts:
This file holds the api that uses the use cases in the application.ts file
import {Router} from 'express'
import {CheckIfEntityExists} from './application';
import {SqlRepository} from './sql-repository';
const router = Router();
router.get("/exists/:filterParam", async (req, res) => {
CheckIfEntityExists(req.params.filterParam, new SqlRepository);
res.end()
});
export default router
Ofc this is just an example, but you get the point of how the project looks like.
From what you can see, its all good until we see the api.ts file. It imports the concrete implementation and injects it into the use case. What if there were much more dependencies to import and use, I do not want the api.ts to be responsible to decide which implementations go to which place, its not its responsibility.
But on the other hand, how should I implement a composition root then? I have no idea how should I construct the full object graph and then pass it to the server object so that the right implementation will go to the right objects.
Thanks in advance!