typescript - Nestjs závislostná injekcia a DDD/čistá architektúra
Experimentujem s Nestjs tým, že sa snažím implementovať štruktúru čistej architektúry a rád by som overil svoje riešenie, pretože si nie som istý, či chápem najlepší spôsob, ako to urobiť. Upozorňujem, že príklad je takmer pseudo-kód a veľa typov chýba alebo sú generické, pretože nie sú predmetom diskusie.
Začínajúc od mojej doménovej logiky, mohol by som ju implementovať v triede ako nasledujúca:
@Injectable()
export class ProfileDomainEntity {
async addAge(profileId: string, age: number): Promise<void> {
const profile = await this.profilesRepository.getOne(profileId)
profile.age = age
await this.profilesRepository.updateOne(profileId, profile)
}
}
Tu potrebujem získať prístup k profileRepository, ale podľa princípov čistej architektúry sa nechcem momentálne zaoberať implementáciou, takže preň píšem rozhranie:
interface IProfilesRepository {
getOne (profileId: string): object
updateOne (profileId: string, profile: object): bool
}
Potom injektujem závislosť do konštruktora ProfileDomainEntity a uistím sa, že bude nasledovať očakávané rozhranie:
export class ProfileDomainEntity {
constructor(
private readonly profilesRepository: IProfilesRepository
){}
async addAge(profileId: string, age: number): Promise<void> {
const profile = await this.profilesRepository.getOne(profileId)
profile.age = age
await this.profilesRepository.updateOne(profileId, profile)
}
}
A potom vytvorím jednoduchú implementáciu v pamäti, ktorá mi umožní spustiť kód:
class ProfilesRepository implements IProfileRepository {
private profiles = {}
getOne(profileId: string) {
return Promise.resolve(this.profiles[profileId])
}
updateOne(profileId: string, profile: object) {
this.profiles[profileId] = profile
return Promise.resolve(true)
}
}
Teraz je čas všetko prepojiť pomocou modulu:
@Module({
providers: [
ProfileDomainEntity,
ProfilesRepository
]
})
export class ProfilesModule {}
Problém tu je, že zjavne ProfileRepository implementuje IProfilesRepository, ale nie je to IProfilesRepository a preto, podľa môjho chápania, je token odlišný a Nest nie je schopný vyriešiť závislosť.
Jediné riešenie, ktoré som na to našiel, je použiť vlastného poskytovateľa na manuálne nastavenie tokenu:
@Module({
providers: [
ProfileDomainEntity,
{
provide: 'IProfilesRepository',
useClass: ProfilesRepository
}
]
})
export class ProfilesModule {}
A upravte ProfileDomainEntity určením tokenu, ktorý sa použije s @Inject:
export class ProfileDomainEntity {
constructor(
@Inject('IProfilesRepository') private readonly profilesRepository: IProfilesRepository
){}
}
Je toto rozumný prístup na riešenie všetkých mojich závislostí alebo som úplne mimo? Existuje nejaké lepšie riešenie? Som pomerne nový vo všetkých týchto veciach (NestJs, čistá architektúra/DDD a aj Typescript), takže tu môžem byť úplne mimo.
Ďakujem
Odpoveď č. 1
Exportujte symbol alebo reťazec spolu s vaším rozhraním s rovnakým názvom
export interface IService {
get(): Promise<string>
}
export const IService = Symbol("IService");
Teraz môžete prakticky používať IService ako rozhranie aj ako token závislosti
import { IService } from '../interfaces/service';
@Injectable()
export class ServiceImplementation implements IService { // Used as an interface
get(): Promise<string> {
return Promise.resolve(`Hello World`);
}
}
import { IService } from './interfaces/service';
import { ServiceImplementation} from './impl/service';
...
@Module({
imports: [],
controllers: [AppController],
providers: [{
provide: IService, // Used as a symbol
useClass: ServiceImplementation
}],
})
export class AppModule {}
import { IService } from '../interfaces/service';
@Controller()
export class AppController {
// Used both as interface and symbol
constructor(@Inject(IService) private readonly service: IService) {}
@Get()
index(): Promise<string> {
return this.service.get(); // returns Hello World
}
}
Odpoveď č. 2
V NestJS nie je možné vyriešiť závislosť cez rozhranie kvôli obmedzeniam/vlastnostiam jazyka (pozri štrukturálne vs nominálne typovanie).
A ak používate rozhranie na definovanie (typu) závislosti, potom musíte použiť reťazcové tokeny. Ale môžete tiež použiť samotnú triedu, alebo jej meno ako reťazcový literál, takže nemusíte spomínať ju pri injekcii, povedzme, v konštruktore závislej triedy.
Príklad :
// *** app.module.ts ***
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AppServiceMock } from './app.service.mock';
process.env.NODE_ENV = 'test'; // or 'development'
const appServiceProvider = {
provide: AppService, // or string token 'AppService'
useClass: process.env.NODE_ENV === 'test' ? AppServiceMock : AppService,
};
@Module({
imports: [],
controllers: [AppController],
providers: [appServiceProvider],
})
export class AppModule {}
// *** app.controller.ts ***
import { Get, Controller } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
root(): string {
return this.appService.root();
}
}
Môžete tiež použiť abstraktnú triedu namiesto rozhrania alebo dať rozhraniu a implementačnej triede podobný názov (a použiť aliasy priamo na mieste).
Áno, v porovnaní s C#/Javou to môže vyzerať ako špinavý hack. Len majte na pamäti, že rozhrania sú iba pre návrhový čas. V mojom príklade AppServiceMock a AppService dokonca ani nededičia z rozhrania ani abstraktnej/základnej triedy (v reálnom svete by to samozrejme mali) a všetko bude fungovať, pokiaľ implementujú metódu root(): string.
Citát z dokumentácie NestJS na túto tému:
OZNÁMENIE
Namiesto vlastného tokenu sme použili triedu ConfigService, a preto sme predefinovali predvolenú implementáciu.
Odpoveď č. 3
Naozaj môžete používať rozhrania, ale aj abstraktné triedy. Jednou z funkcií TypeScriptu je odvodzovanie rozhraní z tried (ktoré sa zachovávajú vo svete JS), takže niečo takéto bude fungovať
IFoo.ts
export abstract class IFoo {
abstract foo(): string;
}
Foo.ts
export class Foo implements IFoo {
foo(): string {
return ""
}
}
const fooProvider = {
provide: IFoo,
useClass: Foo,
};
export class AppController {
constructor(private foo: IFoo) {}
@Get("/")
getRoot() {
return this.foo.foo()
}
}
