Yazılım Geliştirmede Geleceğe Yönelik Dikkat Edilmesi Gereken Kavramlar: Dependency Injection

Abdullah Balıkçı
Crafterdevs
Published in
5 min readApr 5, 2024

Merhabalar ben Crafter Devs ekibinden Abdullah, bu yazımızda Dependency Injection kavramını ele alacağız. Biliyorsunuz ki herhangi bir proje üzerinde çalışıyorsanız, o projenin geleceğe hazır olması önemlidir. Buradaki gelecek kavramını, ilerleyen zamanlardaki gelebilecek değişiklikler ve güncellemeler olarak düşünebilirsiniz. Projelerimizi geliştirirken bağımlılıkları ne kadar iyi bir şekilde ele alırsak, geleceğe hazır ve uyumlu hale gelir. İşte DI kavramı da aslında bağımlılıkları daha iyi yönetmeyi ele alıyor.

Şimdi aklınıza Dependency Injection’nın ne olduğu ve ne anlama geldiğini düşündüğünüzü biliyorum ama ilk önce şu IOC(Inversion Of Control), Interface, Abstraction, Design Pattern, SOLID, ,Bileşen ve Dependency Inversion Prensibine değinmeliyiz. Şimdi Başlayalım …

Not: Buradaki bileşen kavramını, class, interface, metod, property, abstract class veya nesne olarak düşünün.

Not: Aşağıda verdiğim başlıklar, ayrı bir yazı ile ancak açıklanabilir. Burada bilmeyenler için küçük bir bilgi olması amacıyla verilmiştir.

IOC (Inversion of Control) :

Bu prensip, bir bileşenin yaşam döngüsü, bağımlılıkları ve diğer işlemlerinin kontrolünü, kendi içinde değil, dışarıdan sağlanan bir mekanizma tarafından ele alınması anlamına gelir. Aslında bunu yapmakta, gevşek bağlı(Loosely Coupled) bileşenlerin olmasını sağlar. Gevşek bağ, değişikliklere daha uyumlu, bakımı kolay ve test edilebilir bir bileşen sunar. Eğer Bu kısmı anladıysanız, DI(Dependency Injection), IOC prensibini uygulamanın yollarından sadece bir tanesidir.

Not: Türkçe’ye çevirince “Kontrolün Tersine Çevrilmesi” anlamına gelmektedir.

Örnek — 1

A adında bir sınıfımız var, kendi sorumluluğu x nesnesini üretmek olsun ve D adıyla da başka bir sınıfımız var, bunun sorumluluğu da y nesnesini üretmek. Normal şartlarda A sınıfı y nesnesini üretmek için kendi içinde bir nesne oluşturma işlemi yapacaktır(new). İşte IOC bunu tersine çevirerek A’nın kendi ana sorumluluğuyla ilgilenmesini y nesnesini oluşturmak için ayrı bir mekanizma da yani bir konteynır içinde üretilmesini gerektiğini söyler. Tabii bu üretim işleminde genellikle bir interface ile y nesnesi, A sınıfına enjekte edilir.

Örnek — 2

En çok verilen örneği burada tekrar vereceğim. Diyelim bir arabanız var. Arabayı kendiniz sürmek yerine, arabayı sürmek için bir şoför işe alırsanız. Dolayısıyla artık arabanın kontrolün sizden şoförünüze geçiyor. Sonuç olarak artık arabayı kendiniz kullanmak zorunda değilsiniz ve şoförünüz artık arabayı kullanabilir, böylece siz asıl işinize odaklanabilirsiniz.

Konteyner

Buradaki Konteyner kavramını, bağımlılıkları yöneten mekanizma olarak ifade etmek istedim. Her framework’ün kendine has Konteyner’ları vardır. .Net Core için örnekler:

  • Autofact
  • Ninject
  • Unity
  • Castle Windsor
  • Simple Injector
  • Scrutor
  • .Net Core’un kendi yapısında bulunan IServiceCollection örnek verilebilir.

Interface

Genellikle soyutlama katmanının tanımlandığı yapıyı ifade eder. Bu yapı, somut bileşenler arasındaki iletişimi sağlar ve bağımlılıkların dışarıdan enjekte edilmesini kolaylaştırır. Interface, somut bileşenlerin birbirini tanımlamak yerine, birbirleriyle iletişim kurabilecekleri bir ortam sağlar.

Abstraction

Dependency Injection (DI) uygularken kullandığımız abstraction kavramı, genellikle iki somut bileşen arasında oluşturulan interface’i veya abstract class’ı ifade eder. Dependency Injection, somut bileşenler arasında bir soyutlama katmanı oluşturu ve bu katman üzerinden iletişim kurulmasını sağlar. Bu soyutlama genellikle bir interface veya abstract class olarak tanımlanır.

Design Pattern

Design Pattern ise, yazılım geliştirme sürecinde karşılaşılan sorunları çözmek için kullanılan yolları ifade eder.

SOLID

Yazılım geliştirme prensiplerini tanımlayan ve bir yazılımın tasarımını ve bakımını kolaylaştırmayı amaçlayan beş temel prensibi ifade eder. SOLID prensipleri, kodun kalitesini artırmak, esneklik ve bakım kolaylığı sağlamak için kullanılır.

Dependency Inversion

Dependency Inversion, Solid prensiblerinden bir tanesidir.

  • Yüksek seviyeli bileşenler , düşük seviyeli bileşenlere bağımlı olmamalıdır.
  • Soyutlamalar(Abstractions) detaylara bağlı olmaması gerekir. Detaylar soyutlamalara (Abstractions) bağımlı olmalı.

Bu prensbinin temelinde, yüksek seviyeli bileşenler , düşük seviyeli bileşenlerden bağımsız olmalı ve aralarındaki iletişim de Abstraction yapılmalıdır. Eğer bunu yaparsanız bileşenler arasındaki sıkı bağ azalır, Loosely Coupled(gevşek bağlı) bileşenler elde edip, yeni değişikliklere açık bir sistem oluşturursunuz. Son olarak Dependency Injection’da aslında bu prensibi de uygulamanın bir yoludur.

Dependency Injection (DI)

İşte şimdi bu kavramı inceleyebiliriz. DI, Bir tasarım desenidir(design pattern). Bu pattern, bir bileşenin ihtiyaç duyduğu diğer bileşenler (nesneler, metotlar, diğer sınıflar veya servisler gibi) ve kaynakları kendi içinde oluşturmak yerine, dışarıdan sağlanmasını sağlar. Yani, bir sınıfın bir başka sınıfı kendi içinde ‘new’ operatörüyle oluşturması yerine, Soyut bir yapı ve bir konteyner kullanıp, nesneyi dışarıda üretir. Ardından bunu kullanacak bileşene, bu nesneyi sağlar ve bir yaşam döngüsü biçer. Peki bu yaşam döngüleri nedir?

DI Ve Konteyner İçinde Yaşam Döngüsü

public static IServiceCollection AddServiceLayer(this IServiceCollection services,IConfiguration builder)
{
services.AddScoped<IAuthService, AuthService>();
services.AddScoped<IRoleService, RoleService>();

return services;
}
}

Yukarıda örnek bir konteyner görüyorsunuz. Bu .Net Core’un standart konteyner’ı. Burada olan şey aslında şu, IAuthService gördüğünde, AuthService’in bir nesne örneğini oluştur. Solda gördüğünüz AddScoped ise yaşam döngüsünü belirtiyor. Şimdi Örneği daha detaylı açıklıyayım. IAuthService gördüğünde, AuthService’in bir nesne örneğini oluştur. Bu nesne örneği gelen Http isteği boyunca ve talep edilen tüm servisler tarafından kullanılsın, tüm işlemler bitince yok edilebilir anlamına geliyor. Şimdi geçelim yaşam döngülerine;

Konteyner içinde 3 farklı yaşam döngüsü vardır.

AddScoped: Her bir Http isteği için yalnızca bir nesne örneği oluşturur. Oluşturulan örnek, request’in yaşam döngüsü boyunca kullanılır. Yani bu Http isteği içinde farklı sınıflar tarafından bu serviselere erişilse bile , her seferinde aynı örnek kullanılır. Ancak burada önemli olan şey şu, farklı Http istekleri için farklı nesne örnekleri oluşur.

services.AddScoped<IRoleService, RoleService>();

AddSingleton: Uygulamanın tüm yaşam döngüsü boyunca, bütün Http isteklerini aynı örnekle karşılar. Yani tek bir örnek oluşur. Her istek bu örneği kullanır.

services.AddSingleton<ILogService, LogService>();

AddTransient: Bir talep olduğunda yeni bir örnek oluşturur. Bu örneğin yaşamı sadece talep edilen bileşen, ve işlem süresi kadardır. Bu işlem tamamlandığında örnek yok edilir. Her çağrıda veya kullanılması gereken yerde tekrar oluşturulur.

Not: Aşağıdaki kod, örnek olması amacıyla verilmiştir.

services.AddTransient<ITemporaryDatabaseConnection, TemporaryDatabaseConnection>();

Ve şimdi sıra DI yöntemlerinin ne olduğunda.

DI yöntemleri Neledir

  • Constructor Injection:
  • Setter Injection
  • Interface Injection

Constructor Injection


public class TranslatorsController : CustomBaseController
{
private readonly ITranslatorService _translatorService;

public TranslatorsController(ITranslatorService translatorService)
{
_translatorService = translatorService;
}
}

Bir sınıfın, bağımlılıklarını constructor tarafından alması denilebilir. Bağımlılıklar sınıfın içinde doğrudan oluşturulması yerine, sınıfın dışından geçirilmesi anlamına gelir.

Setter Injection


public class TranslatorsController : CustomBaseController
{
private ITranslatorService _translatorService;

public void SetTranslator(ITranslatorService translator)
{
_translatorService= translator;
}
}

Bir sınıfın bağımlılıklarını, sınıfın setter yöntemleri aracılığıyla almasını sağlar. Setter yöntemleri, sınıfın dışındaki bağımlılıkları kabul eder ve bunları sınıfın içindeki uygun alanlara atar. Setter Injection, daha esnektir çünkü bağımlılıklar sınıfın örneği oluşturulduktan sonra değiştirilebilir.

Interface Injection

public interface ITranslatorProvider
{
ITranslatorService Translator{ get; set; }
}


public class TranslatorsController : ITranslatorProvider
{
public ITranslatorService Translator {get;set;}

public async Task<IActionResult> CreateTranslator (CreateTranslatorDto createTranslatorDto)
{
var result = Translator.Create(createTranslatorDto);

return Created(result)
}
}

Bir sınıfın bağımlılıkları, bir interface aracılığıyla enjekte edilir.

DI Örneği :

Soyut IUserService ve onun içeriğini kullanan Somut UserService. Burada önemli olan şey şudur, UserService’in metotlarını ve içeriğini kullanacak olan bileşen(sınıf) bilmez, detaylara hakim değildir. Bileşen sadece IUserService’i ve metot şablonlarını bilir.

public interface IUserService
{
Task<ResponseDto<bool>> CreateUser(RequestRegisterDto requestRegisterDto, CancellationToken cancellationToken);
}

public class UserService : BaseService, IUserService
{
public async Task<ResponseDto<bool>> CreateUser(RequestRegisterDto requestRegisterDto, CancellationToken cancellationToken = default){}

}

Standard .Net Core IOC Container program.cs :

services.AddScoped<IUserService, UserService>();

UserController:

Burada Constructor Injection yapıyoruz.

 public class UsersController : CustomBaseController
{
private IUserService _userService;

public UsersController(IUserService userService)
{
_userService = userService;
}

[HttpPost]
public async Task<IActionResult> CreateUser(RequestRegisterDto requestRegisterDto, CancellationToken cancellationToken)
{
var result = await _userService.CreateUser(requestRegisterDto, cancellationToken);

return await CreateActionResultInstance(result);
}
}

Peki Neden Dependency Injection

Uzun bir okumanın ardından çok fazla bilgi elde ettik ve aslında neden kullanmalıyız sorusunu yazıyı tam anlamıyla okuduysanız zaten anlamışsınızdır ancak son olarak aşağıya tekrardan listeleyeceğim.

Esnek Bileşenler(class, servis, metod, interface, abstract class) sunar.

Esnek olması nedeniyle test edilebilir bileşenlere sahip olursunuz.

Bağımlılık olmaz veya çok az olur.

Bileşenler arasındaki iletişim artık soyutlanır.

Şimdilik yazacaklarım bu kadar, umarım faydalı olur. Kendimi geliştirdikçe güncellemeye devam edeceğim. Kendinize iyi bakın, İyi günler Dilerim.

--

--