Dependency Injection (DI) ve IoC Container Kullanımı

Modern yazılım geliştirme yaklaşımlarının temel taşlarından biri olan Dependency Injection (DI), kodun daha modüler, okunabilir ve test edilebilir olmasını sağlar. Bu makalede, DI kavramını, Inversion of Control (IoC) Container ile nasıl kullanılabileceğini ve .NET Core örnekleriyle nasıl uygulanabileceğini inceleyeceğiz.

Dependency Injection Nedir?

Dependency Injection (DI), bir sınıfın işlevselliği için ihtiyaç duyduğu bağımlılıkların dışarıdan sağlanmasına olanak tanıyan bir tasarım deseni ve pratik bir yaklaşımdır. Bu yöntem sayesinde:

Readability and Modularity (Okunabilirlik ve Modülerlik): Kod daha temiz ve modülerdir.

Loose Coupling (Gevşek Bağlılık): Kodunuzdaki bileşenler birbirine daha az bağımlı olur.

Testability (Test Edilebilirlik): Birimler (units) bağımlılıkları kolayca taklit edilebilir (mock).

IoC Container Nedir?

Inversion of Control (IoC) Container, bağımlılıkları otomatik olarak yöneten ve enjekte eden bir yapıdır. .NET Core ile birlikte, dahili bir IoC Container bulunmaktadır. IoC, uygulamanın kontrol akışını tersine çevirerek nesnelerin yönetimini dışarıdan yapılmasını sağlar. Bu sayede sınıflar, ihtiyaç duydukları bağımlılıkları kendileri yaratmak zorunda kalmaz, bunun yerine dışarıdan sağlanır.

IoC Container Kullanımı

.NET Core projelerinde Host kullanılarak DI yapılandırılması şu şekilde yapılır. ConfigureServices metodu, Program sınıfının içinde Host.CreateDefaultBuilder ile birlikte kullanılır:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace DependencyInjection
{
    class Program
    {
        static void Main(string[] args)
        {
            
            var host = Host.CreateDefaultBuilder(args)
                .ConfigureServices((context, services) =>
                {
                   services.AddSingleton<IMessageWriterWithDI, MessageWriterWithDI>();
                   services.AddHostedService<WorkerWithDI>(); 

                   services.AddHostedService<WorkerWithoutDI>(); 
                })
                .Build();

            host.Run();
        }
    }
}

Dependency Injection Yaşam Süreleri

IoC Container, nesnelerin ömrünü kontrol eder. .NET Core’da bağımlılıklar üç farklı yaşam süresi ile kaydedilebilir:

Singleton: Uygulama başladığından itibaren tek bir nesne kullanılır ve bu nesne tüm uygulama süresince aynı örnekle paylaşılır.
Kullanım Senaryosu: Tek bir örneğin tüm uygulama boyunca kullanılmasını istediğinizde kullanılır. Örneğin, uygulama boyunca aynı veritabanı bağlantısı veya konfigürasyon bilgileri kullanılacaksa tercih edilir.

services.AddSingleton<IMessageWriterWithDI, MessageWriterWithDI>();


Scoped: Her HTTP isteği için aynı nesne örneği kullanılır. Yani, her HTTP isteği arasında nesne örneği paylaşılır, ancak farklı isteklerde farklı nesneler yaratılır.
Kullanım Senaryosu: Bir istemci isteği sırasında, birden fazla servis arasında aynı nesne örneği kullanılacaksa (örneğin, bir web API isteği sırasında) Scoped yaşam süresi kullanılır.

services.AddScoped<MyClass>();


Transient: Her istekte yeni bir nesne örneği oluşturulur. Yani, her bağımlılık çözümlemesi için yeni bir nesne yaratılır.
Kullanım Senaryosu: Servisin her kullanımında yeni bir nesne örneği isteniyorsa kullanılır. Genellikle hafif, kısa ömürlü nesneler için tercih edilir.

services.AddTransient<MyClass>();


DI Kullanımının Avantajları

Bağımlılıkların Yönetimi: DI, sınıfların ihtiyaç duyduğu bağımlılıkları dışarıdan enjekte ederek, sınıfların yalnızca kendileriyle ilgili işlere odaklanmalarını sağlar.

Test Edilebilirlik: DI, testlerde mock (taklit) nesnelerinin kullanılmasına olanak tanır. Bu sayede, bağımlılıklar test aşamasında kolayca değiştirilip izole edilebilir.

Modülerlik: Bağımlılıkların dışarıdan enjekte edilmesi, kodun daha modüler hale gelmesini sağlar. Farklı uygulamalar için farklı implementasyonlar sağlayarak kodun esnekliğini artırabilirsiniz.

Kodun Bakımı: DI ile bağımlılıkları değiştirmek daha kolaydır. Yeni bir implementasyon eklemek veya eski bir implementasyonu değiştirmek, DI konteynerinde tek bir satırla yapılabilir.

IoC (Inversion of Control): DI, kontrolün tersine çevrilmesini sağlar. Yani, uygulama nesnelerinin yaratılması ve yönetilmesi dışarıdan yapılır. Bu sayede uygulama daha esnek ve sürdürülebilir hale gelir.

Dependency Injection ile Örnek Kullanım

Kodun ilk kısmında, DI kullanarak bağımlılıkları yönetiyoruz. Bu, daha modüler, test edilebilir ve bakımı kolay bir yapının oluşmasına yardımcı olur.

Servis Kaydını Yapmak:

services.AddSingleton<IMessageWriterWithDI, MessageWriterWithDI>();


Bu satırda, IMessageWriterWithDI arabiriminin implementasyonu olarak MessageWriterWithDI sınıfı kaydedilmektedir. AddSingleton kullanıldığı için, IMessageWriterWithDI türündeki nesne, uygulama yaşam döngüsü boyunca yalnızca bir kez örneklenecek ve her kullanımda aynı örnek döndürülecektir.

Bağımlılığın Enjekte Edilmesi:

public class WorkerWithDI : BackgroundService
{
    private readonly IMessageWriterWithDI _messageWriter;

    public WorkerWithDI(IMessageWriterWithDI messageWriter)
    {
        _messageWriter = messageWriter;
    }
}


WorkerWithDI sınıfı, IMessageWriterWithDI türünde bir bağımlılık alır. Bu bağımlılık DI konteynerı tarafından otomatik olarak sağlanır. DI sayesinde, WorkerWithDI sınıfı MessageWriterWithDI‘ ı kendisi yaratmak zorunda kalmaz. bunun yerine dışarıdan sağlanan bir nesne ile bağımlılıkları çözülür.

Asenkron İşlem Yapılması:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        _messageWriter.Write($"Worker çalışıyor (DI ile): {DateTimeOffset.Now}");
        await Task.Delay(1000, stoppingToken);
    }
}


ExecuteAsync metodu, arka planda sürekli çalışan bir işçi olarak belirli bir işlemi gerçekleştirir. Burada DI sayesinde, her defasında bağımlılıkları çözmüş olan IMessageWriterWithDI kullanılarak bir mesaj yazdırılır.


Dependency Injection Olmadan Kullanım

İkinci bölümde, DI kullanılmadan aynı işlevi gerçekleştiren bir yapı mevcuttur. Bu senaryoda, bağımlılıkları manuel olarak yaratıyoruz.

Bağımlılıkların Manuel Yaratılması:

public class WorkerWithoutDI : BackgroundService
{
    private readonly MessageWriterWithoutDI _messageWriterWithoutDi;

    public WorkerWithoutDI()
    {
        _messageWriterWithoutDi = new MessageWriterWithoutDI();
    }
}


Bu örnekte, WorkerWithoutDI sınıfı, MessageWriterWithoutDI sınıfına doğrudan bağımlıdır. WorkerWithoutDI sınıfı her seferinde bağımlılıklarını kendisi yaratır. Bu da sınıfın test edilmesini, bakımını ve genişletilebilmesini zorlaştırır.


Bağımlılığı Manuel Olarak Kullanmak:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        _messageWriterWithoutDi.Write($"Worker çalışıyor (DI olmadan): {DateTimeOffset.Now}");
        await Task.Delay(1000, stoppingToken);
    }
}


Burada, WorkerWithoutDI sınıfı yine belirli bir işlemi arka planda yapmaktadır, ancak her seferinde MessageWriterWithoutDI nesnesini manuel olarak yaratmaktadır.


Sonuç

Bu örnekler, Dependency Injection (DI) kullanımının uygulamalarda nasıl daha modüler ve sürdürülebilir kod yapıları oluşturduğunu göstermektedir. Ayrıca, bağımlılıkları manuel olarak yaratmanın getirdiği zorlukları gözler önüne sererken, DI ile bu zorlukların nasıl ortadan kaldırılabileceğini de anlatmaktadır. DI, özellikle büyük ve karmaşık yazılım projelerinde önemli bir rol oynar ve bakım maliyetlerini önemli ölçüde düşürür.

Inversion of Control (IoC), DI’nin temelini oluşturan bir başka önemli kavramdır. IoC, yazılımda “kontrolün tersine çevrilmesi” anlamına gelir. Genellikle, sınıflar ve bileşenler kendi bağımlılıklarını oluşturur ve yönetir. Ancak IoC ile bu sorumluluk dışarıya, yani bir IoC container’a devredilir. Bu sayede sınıflar, ihtiyaç duydukları bağımlılıkları kendileri yaratmak yerine, IoC container’ı tarafından sağlanan nesneleri kullanır.

IoC’nin en büyük avantajlarından biri, yazılımın esnekliğini ve bakımının kolaylaşmasını sağlamasıdır. Bağımlılıklar dışarıdan sağlandığı için, yazılımın parçaları arasındaki sıkı bağlar zayıflar (Loose Coupling). Bu da, yeni özellikler eklerken ya da mevcut işlevselliği değiştirirken daha az risk ve daha az değişiklik gerektirir.

Kısacası, Dependency Injection (DI) ve Inversion of Control (IoC), yazılım geliştirmede esnek ve modüler bi yapının olmazsa olmazıdır. Bu makalemden dilim döndüğünce bildiklerimi aktarmaya çalıştım umarım birilerine bir yerlerde yardımcı olur.


Yeni satırlarda görüşmek üzere . . .