web-dev-qa-db-fra.com

Nestjs Dependency Injection et DDD / Clean Architecture

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

11
Alerosa

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.

9
amankkg

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,
};

6
nolazybits