[英]Nestjs Dependency Injection and DDD / Clean Architecture
我正在通過嘗試實現一個干凈的架構結構來試驗 Nestjs,我想驗證我的解決方案,因為我不確定我是否理解最好的方法。 請注意,該示例幾乎是偽代碼,並且缺少或通用了很多類型,因為它們不是討論的重點。
從我的域邏輯開始,我可能想在 class 中實現它,如下所示:
@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)
}
}
這里我需要訪問profileRepository
,但是遵循clean architecture的原則,我不想被剛才的實現所困擾,所以我為它寫了一個接口:
interface IProfilesRepository {
getOne (profileId: string): object
updateOne (profileId: string, profile: object): bool
}
然后我在ProfileDomainEntity
構造函數中注入依賴項,並確保它遵循預期的接口:
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)
}
}
然后我在 memory 中創建一個簡單的實現,讓我運行代碼:
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)
}
}
現在是時候使用模塊將所有東西連接在一起了:
@Module({
providers: [
ProfileDomainEntity,
ProfilesRepository
]
})
export class ProfilesModule {}
這里的問題是顯然ProfileRepository
實現IProfilesRepository
但它不是IProfilesRepository
因此,據我所知,令牌是不同的,Nest 無法解析依賴關系。
我發現的唯一解決方案是使用自定義提供程序來手動設置令牌:
@Module({
providers: [
ProfileDomainEntity,
{
provide: 'IProfilesRepository',
useClass: ProfilesRepository
}
]
})
export class ProfilesModule {}
並通過指定要與@Inject
一起使用的令牌來修改ProfileDomainEntity
:
export class ProfileDomainEntity {
constructor(
@Inject('IProfilesRepository') private readonly profilesRepository: IProfilesRepository
){}
}
這是用於處理我所有依賴關系的合理方法,還是我完全偏離了軌道? 有沒有更好的解決辦法? 我對所有這些東西(NestJs、干凈的架構/DDD 和 Typescript 以及)都是新手,所以我在這里可能完全錯了。
謝謝
由於語言限制/特性(參見結構與名義輸入) ,因此無法通過NestJS中的接口解析依賴性 。
而且,如果您使用接口來定義(類型)依賴項,那么您必須使用字符串標記。 但是,您也可以使用類本身或其名稱作為字符串文字,因此您不需要在注入期間提及它,比如依賴的構造函數。
例:
// *** 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();
}
}
您還可以使用抽象類而不是接口,或者為接口和實現類提供類似的名稱(並在就地使用別名)。
是的,與C#/ Java相比,這可能看起來像一個骯臟的黑客。 請記住,接口只是設計時間。 在我的例子中, AppServiceMock
和AppService
甚至不是繼承自接口或抽象/基類(在現實世界中,它們當然應該是這樣),只要它們實現方法root(): string
,一切都會起作用。
來自NestJS文檔的關於這個主題的引用:
注意
我們使用ConfigService類而不是自定義標記,因此我們覆蓋了默認實現。
你確實可以使用接口,很好的抽象類。 一個打字稿功能是推斷類的接口(保存在JS世界中),所以像這樣的東西將起作用
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,
};
我使用了一種不同的方法來幫助防止跨多個模塊的命名沖突。
我使用字符串標記和自定義裝飾器來隱藏實現細節:
// injectors.ts
export const InjectProfilesRepository = Inject('PROFILES/PROFILE_REPOSITORY');
// profiles.module.ts
@Module({
providers: [
ProfileDomainEntity,
{
provide: 'PROFILES/PROFILE_REPOSITORY',
useClass: ProfilesRepository
}
]
})
export class ProfilesModule {}
// profile-domain.entity.ts
export class ProfileDomainEntity {
constructor(
@InjectProfilesRepository private readonly profilesRepository: IProfilesRepository
){}
}
它更冗長,但可以從具有相同名稱的不同模塊安全地導入多個服務。
將符號或字符串與具有相同名稱的界面一起導出
export interface IService {
get(): Promise<string>
}
export const IService = Symbol("IService");
現在你基本上可以使用IService
作為接口和依賴項令牌
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
}
}
作為旁注:
如果您遵循 DDD/Clean 架構,則不應從域實體訪問存儲庫。
用例 class 或域服務將使用存儲庫獲取域實體,然后您對其進行操作,當您完成相同的用例/域服務時,將存儲域實體。
領域實體位於架構圖的中心,它不應該依賴於任何其他東西。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.