Dependency Injection Mantığı
Dependency Injection’ın temel prensibi şudur:
Bir sınıf bağımlılığını kendisi oluşturmaz, dışarıdan alır.
Yani:
Bağımlılığı yaratmam, bana verilir.
DI Olmayan Durum
class RegisterUser {
private userRepo = new PrismaUserRepository();
async execute() {
// ...
}
}
Burada:
- Use case doğrudan DB implementasyonuna bağlı
- Test yazmak zor
- Mocklamak zor
- DB değişirse kod değişmek zorunda
Bu tight coupling’dir.
DI Olan Durum
class RegisterUser {
constructor(private userRepo: UserRepository) {}
async execute() {
// ...
}
}
Burada:
- Use case DB’yi bilmez
- Sadece abstraction (interface) bilir
- Implementasyon dışarıdan verilir
Bu loose coupling’dir.
DI’nin Gerçek Amacı
DI’nin amacı:
- Abstraction’a bağımlı olmak
- Concrete implementasyona bağımlı olmamak
Özetle:
High-level module → low-level module’a değil Her ikisi de abstraction’a bağlıdır.
Bu, aslında SOLID içindeki Dependency Inversion Principle’ın uygulanışıdır.
Hard Dependency Örneği
class UserService {
constructor() {
this.emailService = new EmailService();
}
register(user) {
this.emailService.send(user.email, "Hoş geldin!");
}
}
Burada:
UserService → EmailService’e göbekten bağlıdır.
EmailService değişirse UserService değişmek zorunda.
DI ile Çözüm
class UserService {
constructor(emailService) {
this.emailService = emailService;
}
register(user) {
this.emailService.send(user.email, "Hoş geldin!");
}
}
const gmail = new GmailService();
const userSvc = new UserService(gmail);
UserService artık:
- Hangi mail servisi olduğunu bilmez
- Sadece bir “mail gönderen şey” bekler
Bu abstraction’a bağımlılıktır.
DI Neden Kullanılır?
1. Test Edilebilirlik
Gerçek mail servisi yerine fake verilebilir.
const fakeEmailService = {
send: jest.fn()
};
const service = new UserService(fakeEmailService);
Gerçek sistemlere dokunmadan test yapılır.
2. Esneklik
Gmail → SendGrid değişimi:
UserService değişmez. Sadece en başta verilen dependency değişir.
3. Sorumluluk Ayrımı
Bir sınıf:
- İşini yapar
- Bağımlılığını yönetmez
Nesne oluşturma sorumluluğu dışarıdadır.
DI Türleri
1. Constructor Injection (En Doğru Yöntem)
constructor(private repo: UserRepository) {}
Avantaj:
- Immutable
- Zorunlu dependency açık
- Test-friendly
2. Setter Injection
setRepository(repo: UserRepository) {
this.repo = repo;
}
Riskli:
- Nesne yarım oluşabilir
- Zorunlu dependency garanti edilmez
3. Method Injection
execute(repo: UserRepository) {}
Genelde nadir kullanılır.
DI Container Nedir?
Küçük projede:
new A(new B(new C()))
Yönetilebilir.
Ama büyük projede:
- Yüzlerce dependency
- Zincir bağımlılık
- Karmaşık wiring
Bu manuel yapı sürdürülemez.
DI Container:
- Hangi sınıf neye ihtiyaç duyuyor bilir
- Zinciri otomatik kurar
- Lifecycle yönetir (singleton, transient vs.)
Manuel DI (Express Yaklaşımı)
class Logger {
log(message) { console.log(message); }
}
class UserService {
constructor(logger) {
this.logger = logger;
}
create(name) {
this.logger.log(`${name} oluşturuluyor`);
}
}
const logger = new Logger();
const userService = new UserService(logger);
Bağımlılık en alttan yukarı doğru beslenir.
IoC Container (NestJS Yaklaşımı)
@Injectable()
export class LoggerService {}
@Injectable()
export class UserService {
constructor(private logger: LoggerService) {}
}
@Controller()
export class UserController {
constructor(private userService: UserService) {}
}
Burada:
- new yok
- Wiring yok
- Container dependency graph’i yönetir
Bu Inversion of Control’dür.
Kontrol uygulamadan container’a geçer.
DI + Use Case
Clean Architecture’da:
- Use case → abstraction alır
- Infrastructure → implement eder
- Container → bağlar
Örnek test:
const fakeRepo = {
findByEmail: async () => null,
};
const uc = new RegisterUser(fakeRepo);
DB olmadan test yazılabilir.
Araba Benzetmesi
DI olmayan:
Araba kendi motorunu üretir.
DI olan:
Motor dışarıdan takılır.
Motor değişir, şasi değişmez.
Ne Zaman DI Şarttır?
- Clean Architecture varsa
- Use case yapısı varsa
- Unit test yazılıyorsa
- Domain izolasyonu isteniyorsa
Küçük script’lerde gerekmez.