Implementarea rădăcinii compoziției în TypeScript Express
Încerc să înțeleg cum ar trebui să implementez un composition root într-un proiect.
Din ceea ce am citit, dacă folosești rădăcina compoziției în mod greșit (de exemplu, referindu-te la ea în multe locuri din codul aplicației tale), vei ajunge cu locatorul de servicii.
Lasă-mă să-ți arăt un exemplu de proiect fără un rădăcină de compoziție.
Am următoarea structură de proiect:
- server.ts
- domeniu.ts
- aplicație.ts
- api.ts
- repositoriu-sql
server.ts:
Acest fișier importă API-ul și inițializează serverul.
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);
});
domeniu.ts:
Acest fișier conține logica de bază a domeniului.
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:
Acest fișier conține cazurile de utilizare ale aplicației.
import {IRepository} from './domain';
export const CheckIfEntityExists = (filterParam: string, entityRepository: IRepository): boolean => {
let entity = entityRepository.GetEntity(filterParam);
return typeof entity != "undefined";
};
sql-repository.ts:
Acest fișier conține implementarea concretă a interfeței IRepository
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:
Acest fișier conține API-ul care folosește cazurile de utilizare din fișierul application.ts
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
Desigur, acesta este doar un exemplu, dar înțelegi ideea despre cum arată proiectul.
Din ceea ce poți vedea, totul este în regulă până ajungem la fișierul api.ts. Acesta importă implementarea concretă și o injectează în cazurile de utilizare. Ce s-ar întâmpla dacă ar fi mult mai multe dependențe de importat și utilizat, nu vreau ca api.ts să fie responsabil să decidă ce implementări merg în fiecare loc, nu aceasta este responsabilitatea lui.
Dar, pe de altă parte, cum ar trebui să implementez atunci un composition root? Nu am nicio idee cum ar trebui să construiesc întregul graf de obiecte și apoi să-l transmit obiectului server astfel încât implementarea corectă să ajungă la obiectele potrivite.
Mulțumesc anticipat!
Răspuns #1
Definiții
Pentru a oferi o perspectivă și definiții ale termenului Composition Root, iată câteva citate bune de la Mark Seemann în două articole conexe:
Unde ar trebui să compunem grafurile obiectelor?
Cât mai aproape posibil de punctul de intrare al aplicației.
Ce este un Composition Root?
Un Composition Root este un loc (preferabil) unic într-o aplicație unde modulele sunt asamblate împreună.
Rădăcina compoziției este o componentă a infrastructurii aplicației.
Un Rădăcină de Compoziție este specifică aplicației; este ceea ce definește o singură aplicație. După ce ați scris cod frumos și decuplat în întreaga bază de cod, Rădăcina de Compoziție este locul unde în cele din urmă conectați totul, de la accesul la date până la interfețele (utilizatorului).
Implicații
Cu alte cuvinte, api.ts ar putea fi văzut ca punctul de intrare al aplicației tale server, deci este perfect acceptabil să compui graful obiectelor în acesta. Ai putea, de asemenea,
- alege
server.tssau - define un modul DI separat precum
composition-root.tscare face toată compunerea și este importat deserver.tssauapi.ts(chiar mai coerent).
Mai important aici este faptul că ai o locație unică lângă/în punctul de intrare în aplicația ta al proiectului tău, care este responsabilă pentru crearea/compunerea dependențelor.
Exemplu
Să luăm exemplul tău concret și să presupunem că vrem să facem tot ce ține de compunere în composition-root.ts importat de api.ts. Graful tău de dependențe arată astfel (--> înseamnă un import aici):
server.ts --> api.ts --> application.ts --> domain.ts
--> sql-repository.ts
Totul, cu excepția composition-root.ts, este decuplat de dependențele sale. Injecția prin constructor ar putea fi folosită ca în exemplul articolului, sau orice altă metodă de injecție, în funcție de limbajul/framework-ul/stilul de codare. Exemplul tău arată deja destul de bine, hai să adăugăm un strat de abstractizare pentru baza de date pentru repository și să abstractizăm compunerea din api.ts.
sql-repository.ts:
export class SqlRepository implements IRepository {
constructor(private db: DB) {}
...
}
api.ts:
import {CheckIfEntityExists} from "./composition-root"
...
router.get("/exists/:filterParam", async (req, res) => {
CheckIfEntityExists(req.params.filterParam);
res.end()
});
composition-root.ts:
import {CheckIfEntityExists} from './application';
import {SqlRepository} from './sql-repository';
const db = new DB();
const sqlRepository = new SqlRepository(db);
// We do partial application here to free api.ts from
// knowing the concrete repository.
const _CheckIfEntityExists = (filterParam: string) =>
CheckIfEntityExists(filterParam, sqlRepository);
export { _CheckIfEntityExists as CheckIfEntityExists };
Per total, ai încapsulat frumos dependențele tale într-un singur loc composition-root.ts, iar alte coduri din straturile mai interioare ale arhitecturii aplicației tale nu știu nimic despre construcția lor.
Sper că asta ajută.
