typescript - Nestjs Bağımlılık Enjeksiyonu ve DDD/Temiz Mimari
Temiz bir mimari yapıyı uygulamaya çalışarak Nestjs ile denemeler yapıyorum ve çözümümü doğrulamak istiyorum çünkü en iyi yolu anlayıp anlamadığım konusunda emin değilim. Lütfen örneğin neredeyse sözde kod olduğunu ve birçok türün eksik veya genel olduğunu, çünkü tartışmanın odağında olmadıklarını unutmayın.
Alanım mantığından başlayarak, bunu aşağıdaki gibi bir sınıfta uygulamak isteyebilirim:
@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)
}
}
Burada profileRepository'e erişim sağlamam gerekiyor, ancak temiz mimari prensiplerini takip ederek şu an uygulama detaylarıyla uğraşmak istemiyorum, bu yüzden bunu için bir arayüz yazıyorum:
interface IProfilesRepository {
getOne (profileId: string): object
updateOne (profileId: string, profile: object): bool
}
Daha sonra bağımlılığı ProfileDomainEntity yapıcısına enjekte ediyorum ve bunun beklenen arayüzü takip edeceğinden emin oluyorum:
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)
}
}
Ve sonra kodu çalıştırmama izin veren basit bir hafızada uygulama oluşturuyorum:
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)
}
}
Şimdi her şeyi bir modül kullanarak birbirine bağlama zamanı:
@Module({
providers: [
ProfileDomainEntity,
ProfilesRepository
]
})
export class ProfilesModule {}
Buradaki sorun, açıkça ProfileRepository'nin IProfilesRepository'yi uyguluyor olması ama IProfilesRepository olmaması ve bu nedenle, anladığım kadarıyla, token farklı ve Nest bağımlılığı çözmekte başarılı olamıyor.
Bunun için bulduğum tek çözüm, token'ı manuel olarak ayarlamak için özel bir sağlayıcı kullanmaktır:
@Module({
providers: [
ProfileDomainEntity,
{
provide: 'IProfilesRepository',
useClass: ProfilesRepository
}
]
})
export class ProfilesModule {}
Ve @Inject ile kullanılacak tokeni belirterek ProfileDomainEntity'yi değiştirin:
export class ProfileDomainEntity {
constructor(
@Inject('IProfilesRepository') private readonly profilesRepository: IProfilesRepository
){}
}
Tüm bağımlılıklarımla başa çıkmak için kullanmak üzere bu makul bir yaklaşım mı, yoksa tamamen yanlış yolda mıyım? Daha iyi bir çözüm var mı? Tüm bunlara (NestJs, temiz mimari/DDD ve Typescript) oldukça yeni olduğum için burada tamamen yanlış olabilirim.
Teşekkürler
Cevap #1
Aynı isimle bir sembolü veya bir stringi arayüzünüzle birlikte dışa aktarın
export interface IService {
get(): Promise<string>
}
export const IService = Symbol("IService");
Artık IService'yi hem arayüz hem de bağımlılık belirteci olarak temel olarak kullanabilirsiniz
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
}
}
Cevap #2
Dil sınırlamaları/özellikleri nedeniyle (bkz. yapısal vs nominal tipleme) NestJS'te bağımlılığı arayüz üzerinden çözmek mümkün değildir.
Ve eğer bir bağımlılığın (türü) tanımlamak için bir arayüz kullanıyorsanız, o zaman string tokenlar kullanmanız gerekir. Ama aynı zamanda sınıfın kendisini veya adını bir string literal olarak da kullanabilirsiniz, böylece bağımlının yapıcısında enjeksiyon sırasında bunu belirtmenize gerek kalmaz.
Örnek :
// *** 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();
}
}
Bir arayüz yerine bir soyut sınıf da kullanabilir veya hem arayüze hem de uygulama sınıfına benzer bir isim verebilirsiniz (ve yerine takma adlar kullanabilirsiniz).
Evet, C#/Java ile karşılaştırıldığında bu kirli bir hile gibi görünebilir. Sadece arayüzlerin yalnızca tasarım zamanı için olduğunu aklınızda bulundurun. Örneğimde, AppServiceMock ve AppService hatta arayüzden veya soyut/temel sınıftan bile türemiyor (gerçekte, tabii ki türemeliler) ve root(): string yöntemini uygulatabildikleri sürece her şey çalışacaktır.
Bu konudaki NestJS dokümanlarından alıntı:
DUYURU
Özel bir token yerine ConfigService sınıfını kullandık ve bu nedenle varsayılan uygulamayı geçersiz kıldık.
Cevap #3
Gerçekten de arayüzler kullanabilirsiniz, iyi de soyut sınıflar. Bir TypeScript özelliği, sınıflardan (JS dünyasında tutulan) arayüz çıkarsamaktır, bu yüzden buna benzer bir şey işe yarayacaktır
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()
}
}
