J'expérimente avec Nestjs en essayant d'implémenter une structure à architecture propre et je voudrais valider ma solution car je ne suis pas sûr de comprendre la meilleure façon de le faire. Veuillez noter que l'exemple est presque un pseudo-code et que de nombreux types sont manquants ou génériques car ils ne sont pas au centre de la discussion.
À partir de ma logique de domaine, je pourrais vouloir l'implémenter dans une classe comme la suivante:
@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)
}
}
Ici, je dois accéder à profileRepository
, mais en suivant les principes de l'architecture propre, je ne veux pas être dérangé par l'implémentation tout de suite, donc j'écris une interface pour cela:
interface IProfilesRepository {
getOne (profileId: string): object
updateOne (profileId: string, profile: object): bool
}
Ensuite, j'injecte la dépendance dans le constructeur ProfileDomainEntity
et je m'assure que ça va suivre l'interface attendue:
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)
}
}
Et puis je crée une implémentation simple en mémoire qui me permet d'exécuter le code:
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)
}
}
Il est maintenant temps de tout connecter ensemble à l'aide d'un module:
@Module({
providers: [
ProfileDomainEntity,
ProfilesRepository
]
})
export class ProfilesModule {}
Le problème ici est qu'évidemment ProfileRepository
implémente IProfilesRepository
mais ce n'est pas IProfilesRepository
et donc, si je comprends bien, le jeton est différent et Nest n'est pas en mesure de résoudre la dépendance .
La seule solution que j'ai trouvée est d'utiliser un fournisseur personnalisé pour définir manuellement le jeton:
@Module({
providers: [
ProfileDomainEntity,
{
provide: 'IProfilesRepository',
useClass: ProfilesRepository
}
]
})
export class ProfilesModule {}
Et modifiez le ProfileDomainEntity
en spécifiant le jeton à utiliser avec @Inject
:
export class ProfileDomainEntity {
constructor(
@Inject('IProfilesRepository') private readonly profilesRepository: IProfilesRepository
){}
}
Est-ce une approche raisonnable à utiliser pour gérer toutes mes dépendances ou suis-je complètement hors-piste? Y a-t-il une meilleure solution? Je suis relativement nouveau dans toutes ces choses (NestJs, architecture propre/DDD et TypeScript également), donc je peux me tromper totalement ici.
Merci
Il n'est pas possible de résoudre la dépendance par l'interface dans NestJS en raison des limitations/fonctionnalités du langage (voir typage structurel vs nominal) .
Et, si vous utilisez une interface pour définir un (type de) dépendance, vous devez utiliser des jetons de chaîne. Mais, vous pouvez également utiliser la classe elle-même, ou son nom comme littéral de chaîne, vous n'avez donc pas besoin de le mentionner lors de l'injection dans, disons, le constructeur de la personne à charge.
Exemple:
// *** 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();
}
}
Vous pouvez également utiliser une classe abstraite au lieu d'une interface ou donner un nom similaire à la classe d'interface et à la classe d'implémentation (et utiliser des alias sur place).
Oui, en comparaison avec C #/Java, cela pourrait ressembler à un hack sale. Gardez à l'esprit que les interfaces sont uniquement conçues au moment de la conception. Dans mon exemple, AppServiceMock
et AppService
n'héritent même pas de l'interface ni de la classe abstract/base (dans le monde réel, ils devraient bien sûr) et tout fonctionnera tant qu'ils implémenteront la méthode root(): string
.
Citation de les documents NestJS sur ce sujet :
REMARQUER
Au lieu d'un jeton personnalisé, nous avons utilisé la classe ConfigService et, par conséquent, nous avons remplacé l'implémentation par défaut.
Vous pouvez en effet utiliser des interfaces, des classes bien abstraites. Une fonctionnalité TypeScript déduit l'interface des classes (qui sont conservées dans le monde JS), donc quelque chose comme ça fonctionnera
IFoo.ts
export abstract class IFoo {
public abstract bar: string;
}
Foo.ts
export class Foo
extends IFoo
implement IFoo
{
public bar: string
constructor(init: Partial<IFoo>) {
Object.assign(this, init);
}
}
const appServiceProvider = {
provide: IFoo,
useClass: Foo,
};