Dependency Injection, bir sınıfın ihtiyaç duyduğu bağımlılıkların dışarıdan sağlanması ile daha esnek, test edilebilir ve sürdürülebilir bir mimari oluşturmayı amaçlayan bir design pattern’dır.
Problem ve Çözüm
Yazılım geliştirirken bir kodun sadece “çalışıyor” olması genellikle yeterli değildir. Uzun vadede sürdürülebilir projeler geliştirmek istiyorsak, kodun düzenli, esnek, test edilebilir ve verimli olması da en az o kadar önemlidir.
Bu hedeflere ulaşmak için yazılım dünyasında pek çok yaklaşım benimsenmiştir. Yazılım mimarileri (architecture), design pattern’lar, best practice’ler, clean code prensipleri ve daha pek çoğu. Dependency Injection (DI) da bu yaklaşımlardan biridir ve özellikle bağımlılık yönetimi konusunda modern projelerin vazgeçilmezidir.
DI, sınıflar arasındaki bağımlılıkların daha düzenli ve kontrol edilebilir hale getirilmesini sağlar. Üstelik sadece bir teknolojiye ya da dile özgü değildir. ASP.NET Core, Spring Boot gibi modern framework’lerin çoğu, DI desteğini yerleşik olarak sunar. Aslında, bu framework’lerle çalışan birçok geliştirici, farkında olmasa bile zaten DI kullanıyordur.
Bu blog yazısında amacım, dependency injection kavramını tüm yönleriyle açıklamak ve uygulamalı örneklerle zihnimizde netleşmesini sağlamak.
⚠️ Bağımlılık Sorunu
Geliştirdiğimiz projelerde, bir sınıfın başka sınıflara ihtiyaç duyması çok normal bir durumdur. Bu ihtiyaçlar, çoğu zaman aşağıdaki gibi doğrudan sınıf içerisinde tanımlanarak karşılanır.
// Bad class structure - Tightly Coupled
public class UserService
{
private readonly EmailService _emailService = new EmailService();
// Other codes
}
Bu örnekte UserService
, doğrudan EmailService
sınıfına bağımlıdır. Eğer EmailService
sınıfı yoksa, UserService
çalışamaz. Bu duruma tightly coupled (sıkı bağlılık) denir. Üstelik EmailService
instance’ı sınıfın içinde new
anahtar kelimesiyle oluşturulmuş.
Tightly coupled sınıflar geliştiricilerin hiç istemediği bir durumdur.
Çünkü şu sorunlara neden olur:
- Test etmesi zordur.
- Kodun yeniden kullanımı kısıtlanır.
- Değiştirilebilirlik azalır (örneğin başka bir e-posta servis sağlayıcısına geçmek zorlaşır).
- Kodun bağımlılıklarını yönetmek karmaşık hale gelir.
✅ Çözüm: Dependency Injection ile Loosely Coupled
Amacımız, sınıfların ihtiyaç duyduğu bağımlılıkları kendileri oluşturmaları yerine dışarıdan almasıdır. Bu yaklaşım, bağımlılıkları daha yönetilebilir hale getirir ve sınıflar arasındaki bağlantıyı gevşek hale getirir. Buna da loosely coupled (gevşek bağlılık) denir.
// Good class structure - DI and Loosely Coupled
public class UserService
{
private readonly IEmailService _emailService;
public UserService(IEmailService emailService)
{
_emailService = emailService;
}
// Other codes
}
Yukarıdaki örnekte, DI yöntemi kullanarak UserService
sınıfının, IEmailService
arayüzünü dışarıdan almasını sağlıyoruz.
Gördüğünüz gibi IEmailService
instance’ı new
anahtar kelimesiyle içeride oluşturulmuyor. Peki bu instance nerede oluşturuluyor? Hemen oraya gelelim.
ASP.NET Core uygulamalarında, bağımlılıkları uygulama başlatılırken DI Container yardımıyla aşağııdaki gibi register ederiz.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<IEmailService, EmailService>();
// Other codes
Bu satırla birlikte, uygulama boyunca IEmailService
ihtiyaç duyulduğunda, EmailService
sınıfından bir instance oluşturulup enjekte edilir. Yani sorumuzun cevabı, dependency injection mekanizması (DI Container) tarafından instance’lar üretilir. Bu sayede:
- Kod daha modüler hale gelir.
- Test yazmak çok daha kolaylaşır (örneğin mock servisler kullanılabilir).
- Uygulama, değişikliklere karşı daha esnek olur.
Temel Kavramlar
Dependency Injection konusunu daha iyi anlamamız için bilmemiz gereken bazı temel kavramlara değinelim.
Kavram | Açıklama |
---|---|
Design Pattern | Yazılım geliştirmede sık karşılaşılan problemlere yönelik, defalarca test edilmiş ve etkili olduğu kanıtlanmış çözüm şablonlarıdır. ❗ Design pattern ≠ kod parçası ✔️ Bir yapısal fikir, bir tasarım yaklaşımıdır. |
Inversion of Control (IoC) | Kod içerisindeki kontrolün sınıfın kendisinden alınıp dış bir yapıya devredilmesidir. ✔️ Dependency Injection, IoC prensibinin en yaygın uygulamalarından biridir. |
DI Container (ya da IoC Container) | Sınıfların bağımlılıklarını çözümleyen ve yaşam döngülerini yöneten yapıdır. Hangi sınıfa hangi bağımlılığın verileceğini bilir ve bu nesneleri üretir. |
Loosely Coupled | Bileşenlerin birbirine gevşek bağlı olmasıdır. Yani bir sınıftaki değişiklik, diğer sınıfları minimum düzeyde etkiler. Bu durum, bakım ve test süreçlerini kolaylaştırır. |
Tightly Coupled | Bileşenlerin birbirine sıkı bağlı olmasıdır. Bir sınıfta yapılan değişiklik, doğrudan diğerlerini etkileyebilir. Kodun test edilebilirliği ve esnekliği azalır. |
Dependency Injection Uygulama Yöntemleri
Dependency Injection, üç farklı şekilde uygulanabilir:
- Constructor Injection
- Property Injection
- Method Injection
Bu yöntemler arasında en yaygın ve önerilen yaklaşım Constructor Injection’dır. ASP.NET Core’da da varsayılan olarak bu yöntem kullanılır. Diğer iki yöntem ise özel senaryolar dışında pek tercih edilmez.
Constructor Injection
Constructor Injection, bağımlılıkların sınıfın constructor’ı aracılığıyla dışarıdan verilmesidir (yukarıdaki kod parçalarında da yazdığımız gibi). Bu yaklaşım, sınıfın ihtiyacı olan tüm bileşenleri doğrudan belirtmesini sağlar, böylece sınıfın bağımlılıkları daha net olur.
public interface IMessageService
{
void Send(string message);
}
public class EmailService : IMessageService
{
public void Send(string message)
{
Console.WriteLine($"Email sent: {message}");
}
}
public class NotificationManager
{
private readonly IMessageService _messageService;
// Constructor Injection
public NotificationManager(IMessageService messageService)
{
_messageService = messageService;
}
}
Yukarıdaki örnekte NotificationManager
sınıfı, IMessageService
üzerinden soyutlanmış bir servise ihtiyaç duyuyor. Bu yaklaşım sayesinde:
- Sınıf daha kolay test edilebilir.
- Alternatif servislerle (örneğin
SmsService
) rahatlıkla çalışabilir. - Kod daha sade, anlaşılır ve sürdürülebilir olur.
Dependency Lifetime
Bir sınıf nesnesinin (instance) ne kadar süreyle yaşayacağını belirlemek önemlidir. ASP.NET Core’da servisleri IServiceCollection
üzerinden DI Container’a kaydederken, bu yaşam süresini de aşağıdaki gibi belirtmemiz gerekir.
services.AddSingleton<IMyService, MyService>();
services.AddScoped<IMyService, MyService>();
services.AddTransient<IMyService, MyService>();
Her bir lifetime türünün davranışı farklıdır.
Lifetime | Açıklama | Ne Zaman Kullanılır? |
---|---|---|
Singleton | Uygulama başlatıldığında bir kez oluşturulur ve her yerde aynı instance kullanılır. | Statik veri tutan, paylaşımlı ve thread-safe servislerde tercih edilir. Örnek: services.AddSingleton<IEmailService, EmailService>(); |
Scoped | Her HTTP isteği için bir instance oluşturulur. Aynı istek içerisinde aynı instance kullanılır. | Web API servislerinde yaygın olarak kullanılır. Örnek: services.AddScoped<IUserService, UserService>(); |
Transient | Her ihtiyaç duyulduğunda (injection talebinde) yeni bir instance oluşturulur. | Hafif, state tutmayan ve hızlı nesneler için uygundur. Örnek: services.AddTransient<IReportGenerator, ReportGenerator>(); |
DI Uygulaması: Console Projesi
Dependency Injection’ı daha iyi anlamak için sıfırdan kendimiz yazalım. Bu yüzden şimdi basit bir Console uygulaması üzerinden ilerleyelim.
Console projelerinde ASP.NET Core’daki gibi hazır bir DI mekanizması yoktur. Bu yüzden Microsoft.Extensions.DependencyInjection paketini projeye manuel olarak dahil etmemiz gerekir. (Ya da başka bir DI kütüphanesi de tercih edebiliriz ama biz burada Microsoft’un geliştirdiğini kullanacağız.)
Senaryomuz şu şekilde: Kullanıcılara email veya SMS ile bildirim gönderen bir yapı tasarlıyoruz. Gerçek mesaj göndermek yerine, mesajları konsola yazdıracağız.
IMessageSender Arayüzü ve İmplementasyonlar
Amacımız loosely coupled ve kolay değiştirilebilir bir yapı kurmak.
// Senders.cs
public interface IMessageSender
{
void Send(string message);
}
public class EmailSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine($"Email sent: {message}");
}
}
public class SmsSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine($"SMS sent: {message}");
}
}
NotificationService Sınıfı
Bu yapı sayesinde, NotificationService
yalnızca IMessageSender
arayüzünü tanır. Hangi somut (concrete) sınıfın kullanılacağını dışarıdan biz belirleriz. Bu da kodun test edilebilirliğini ve esnekliğini artırır.
// NotificationService.cs
public class NotificationService
{
private readonly IMessageSender _sender;
// Constructor Injection
public NotificationService(IMessageSender sender)
{
_sender = sender;
}
public void Notify(string message)
{
_sender.Send(message);
}
}
✔️ NotificationService
, mesaj gönderme işini kendi yapmıyor. Bunun yerine bu sorumluluğu IMessageSender
’a devrediyor. Hangi tip mesaj gönderici kullanılacaksa (Email mi, SMS mi), bu dışarıdan geliyor. Böylece sınıfın tek bir sorumluluğu olmuş oluyor.
Program.cs: DI Container ile Bağlantı
Burada bağımlılıkları kaydediyoruz.
// Program.cs
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
// DI registers
services.AddTransient<IMessageSender, EmailSender>(); // Here you could also give SmsSender
services.AddTransient<NotificationService>();
var serviceProvider = services.BuildServiceProvider();
// Get an instance of NotificationService from the DI container.
// No need to manually instantiate it using 'new'.
var notifier = serviceProvider.GetRequiredService<NotificationService>();
notifier.Notify("Hello Dependency Injection!");
✔️ Sadece tek satırı değiştirerek EmailSender yerine SmsSender kullanılmasını sağlayabiliriz. Hiçbir sınıfı değiştirmemize gerek yok. İşte buna loosely coupled diyoruz.
Microsoft.Extensions.DependencyInjection Nedir?
Bu paket, .NET ekosisteminde bağımlılık enjeksiyonu (DI) işlemleri için kullanılan Microsoft tarafından sunulan bir çözümdür.
- ASP.NET Core projelerinde otomatik olarak gelir.
- Console gibi projelere ise manuel olarak NuGet üzerinden eklenmesi gerekir.
Bu proje örneğinde, DI mantığını framework bağımsız bir şekilde nasıl uygulayabileceğimizi gördük.
🔗 GitHub – Dependency Injection
Projeyi klonlayarak doğrudan kendi bilgisayarınızda test edebilirsiniz.
DI Uygulaması: ASP.NET Core Projesi
Console uygulamasında DI’ı sıfırdan kurmuştuk. Şimdi ise ASP.NET Core tarafına geçiyoruz. ASP.NET Core projelerinde Dependency Injection yapısı framework sayesinde yerleşik olarak gelir.
Yine aynı senaryodayız: Kullanıcılara email ya da SMS ile bildirim gönderen bir yapı kuruyoruz. Gerçek mesajlar yerine çıktılar sadece konsola yazılacak.
IMessageSender Arayüzü ve Uygulamaları
Amacımız yine loosely coupled ve kolay değiştirilebilir bir yapı kurmak.
// Services/Senders.cs
public interface IMessageSender
{
void Send(string message);
}
public class EmailSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine($"Email sent: {message}");
}
}
public class SmsSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine($"SMS sent: {message}");
}
}
NotificationService Sınıfı
Amacımız uygulama hangi yöntemi kullanarak bildirim gönderecekse, onu sadece bir noktada tanımlamak (Program.cs). Diğer sınıflar detayları bilmesin, sadece IMessageSender
üzerinden çalışsın.
// Services/NotificationService.cs
public class NotificationService
{
private readonly IMessageSender _sender;
// Constructor injection
public NotificationService(IMessageSender sender)
{
_sender = sender;
}
public void Notify(string message)
{
_sender.Send(message);
}
}
NotificationService
, dışarıdan IMessageSender
bekliyor. Hangi sınıf verilecek (Email mi SMS mi vs.), bu sınıfı ilgilendirmiyor. Bu da daha sade, daha test edilebilir bir yapı sağlıyor.
Bağımlılıkların Kayıt Edilmesi (Program.cs)
Aşağıdaki örnekte EmailSender
sınıfı kullanılıyor. İleride SmsSender
’a geçmek istersek tek yapmamız gereken kayıt satırını değiştirmek.
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Dependency injection registers
builder.Services.AddTransient<IMessageSender, EmailSender>();
builder.Services.AddTransient<NotificationService>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
Not: AddTransient
, her istek için yeni bir instance oluşturur.
API Controller
Artık servisimizi bir controller üzerinden kullanabiliriz. Dependency Injection sayesinde, NotificationService
de otomatik olarak dışarıdan enjekte edilecek. Çünkü daha önce builder.Services.AddTransient<NotificationService>()
satırıyla DI container’a ekledik.
// Controllers/NotificationController.cs
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class NotificationController : ControllerBase
{
private readonly NotificationService _notificationService;
public NotificationController(NotificationService notificationService)
{
_notificationService = notificationService;
}
[HttpPost]
public IActionResult Send([FromBody] string message)
{
_notificationService.Notify(message);
return Ok("Notification sent.");
}
}
ASP.NET Core projelerinde Dependency Injection yapısını kullanmak son derece doğal ve basittir çünkü altyapı bunu desteklemek üzere inşa edilmiştir. Bu sayede bağımlılık yönetimi kolaylaşır, kod test edilebilirliği artar ve uygulama yapısı daha esnek hale gelir.
Bu örnekte olduğu gibi bir interface üzerinden hareket ederek uygulama içinde hangi somut sınıfın kullanılacağına tek bir noktadan karar veriyoruz. Bu da uygulamayı daha sürdürülebilir ve geliştirilebilir hale getiriyor.
🔗 GitHub – Dependency Injection
Projeyi klonlayarak doğrudan kendi bilgisayarınızda test edebilirsiniz.