Dependency Injection ve Basit Bir IoC Container Nasıl Yapabiliriz? (.net)
Senaryo
Bir sipariş sistemimiz olduğunu düşünelim, bu sistemde yeni sipariş oluşturulduğunda ve var olan bir sipariş güncellendiğinde siparişi oluşturan kullanıcıya mail gitmeli. Mail gönderme işlemi sistem kurulduğunda gmail altyapısını kullanıyorken ilerleyen zamanlarda outlook altyapısına veya başka bir mail servisine geçiş yapabileceği öngörülüyor. Bu değişime direnmeyen ve kolay güncellenebilen bir kod yapısı oluşturmak istiyoruz. Bunu nasıl yapabiliriz?
Konsol Uygulaması ile Basit Bir Yapı Oluşturuyoruz
Oluşturduğumuz yapıda sipariş işlemleri OrderController sınıfı üzerinden gerçekleştiriliyor.
- Bir sipariş oluşturmak için OrderController sınıfının Post metodunu tetiklememiz gerekiyor.
- Bir sipariş güncellemek için ise Put metodunu tetiklememiz gerekiyor.
public class OrderController
{
public void Post()
{
Console.WriteLine("handled order post action");
}
public void Put()
{
Console.WriteLine("handled order put action");
}
}
Bir sipariş oluşturmak için Program.cs üzerinden şöyle bir kod çağrısı yapıyoruz.
var orderController = new OrderController();
orderController.Post();
Sipariş oluşturulduğunda ve eklendiğinde mail atabilmek için bir servis sınıfımız var.
public class GmailService
{
public void SendEmail(string sender, string reciever, string content)
{
Console.WriteLine($"Gmail Service sent an email.\nsender:{sender}\nreciever:{reciever}\ncontent:{content}");
}
}
OrderController bu sınıfı kullanarak sipariş oluştuğunda ve güncellendiğinde SendEmail metodunu çağırıyor.
public class OrderController
{
public void Post()
{
Console.WriteLine("handled order post action");
var mailService = new GmailService();
mailService.SendEmail(sender: "sender@gmail.com", reciever: "reciever@gmail.com", content: "an email content");
}
public void Put()
{
Console.WriteLine("handled order put action");
var mailservice = new GmailService();
mailservice.SendEmail(sender: "sender@gmail.com", reciever: "reciever@gmail.com", content: "an email content");
}
}
Programı çalıştırdığımızda şu çıktıyı alıyoruz:
handled order post action
Gmail Service sent an email.
sender:sender@gmail.com
reciever:reciever@gmail.com
content:an email content
Bağımlılıkların Çözümlenmesi
Uygulama şuan çalışıyor ve ihtiyaca yanıt üretiyor ancak OrderController içerisinde yeni GmailService nesnesi oluşturarak sıkı bağlı bir yapı kuruldu. OrderController GmailService sınıfına bağımlı ve bu bağımlılığın soyutlamaya çevrilmesi gerekiyor. Neden? Çünkü Outlook servisi kullanılmaya başlandığında mail gönderimiyle alakalı neredeyse tüm kod satırlarının düzenlenmesi gerekecek ve tekrar bu tip bir değişim ihtiyacında aynı işlemler tekrarlanacak. Bu da kodun değişime direçli bir hale gelmesine neden olduğu için bunu istemiyoruz. Ne istiyoruz? Esnek bağlı bir yapı. Bunu nasıl yapacağız? OrderController GmailService sınıfına değil bu sınıfın soyutlamasına bağımlı olacak ve GmailService de soyutlamadan türetilecek. Böylece iki sınıf bir protokol imzalamış gibi olacak ve bu kurallar bütünü üzerinden anlaşmaları sayesinde yeni oluşturulacak bir mail servisi için kod hızlıca uyarlanabilecek.
Kötü Durum
Projeye yeni bir mail servisi eklendi.
public class OutlookMailService
{
public void EmailSend(Mail mail)
{
Console.WriteLine($"Outlook service is sent an email!\nreciever:{mail.Reciever}\ncontent:{mail.Content}");
}
}
public class Mail
{
public string Reciever { get; set; }
public string Content { get; set; }
}
Sipariş Yönetim sınıfı bu değişim ile çalışamaz duruma geldi ve düzenleme gerekti.
public class OrderController
{
public void Post()
{
var mailService = new OutlookMailService();
mailService.EmailSend(mail: new Mail { Content = "an email content", Reciever = "reciever@gmail.com" });
}
public void Put()
{
var mailService = new OutlookMailService();
mailService.EmailSend(mail: new Mail { Content = "your order updated", Reciever = "reciever@gmail.com" });
}
}
Olması Gereken Durum
İki mail servisi ve daha sonra eklenebilecek herhangi bir mail servisi soyut bir servisten türemeli idi.
public interface IMailService
{
void SendMail(Mail mail);
}
Mail servislerinin tümü bu soyutlamadan türetilerek Sipariş Yönetim sınıfının bağımlılığı esnek hale getirilseydi değişim gerekmeyecekti.
public class OutlookMailService : IMailService
{
public void SendMail(Mail mail)
{
Console.WriteLine($"Outlook service is sent an email!\nreciever:{mail.Reciever}\ncontent:{mail.Content}");
}
}
public class GmailService : IMailService
{
public void SendMail(Mail mail)
{
Console.WriteLine($"Gmail Service sent an email.\nsender:sender\nreciever:{mail.Reciever}\ncontent:{mail.Content}");
}
}
İki servis de aynı soyutlamadan türediği için metod isimleri ve parametreleri ortaklaştırıldı böylece yeni eklenen servisler de bu ortak imzaya uyacak ve Sipariş Yönetim sınıfı mail servis değişiminden etkilenmeyecek.
public class OrderController
{
public void Post()
{
var mailService = new OutlookMailService();
mailService.SendMail(mail: new Mail { Content = "an email content", Reciever = "reciever@gmail.com" });
}
public void Put()
{
var mailService = new OutlookMailService();
mailService.SendMail(mail: new Mail { Content = "your order updated", Reciever = "reciever@gmail.com" });
}
}
Bu durumda yeni bir mail servisi eklenirse yalnızca yeni oluşturulan nesne tipleri değişerek sistem çalışmaya devam edecek ama hala birden fazla yerde değişiklik yapmamız gerekiyor. Bunu engellemenin yolu ise bağımlılığı enjekte etmek ve new anahtar kelimesini kullanmaktan kaçınmak.
OrderController bu mail servis nesnesini dışarıdan alıyor olsaydı nasıl olurdu?
public class OrderController
{
IMailService _mailService;
public OrderController(IMailService mailService)
{ _mailService = mailService;
}
public void Post()
{
_mailService.SendMail(mail: new Mail { Content = "an email content", Reciever = "reciever@gmail.com" });
}
public void Put()
{
_mailService.SendMail(mail: new Mail { Content = "your order updated", Reciever = "reciever@gmail.com" });
}
}
Bu durumda Program.cs üzerinden OrderController nesnesi oluşturulurken artık hangi mail servisini kullanacağı bilgisi için bir parametre istiyor.
Gmail servisini kullanmak istiyorsak:
var orderController = new OrderController(new GmailService());
orderController.Post();
Outlook servisini kullanmak istiyorsak:
var orderController = new OrderController(new OutlookMailService());
orderController.Post();
Proje kaynak kodunda başka herhangi bir noktada değişim yapmadan iki servis arasında geçiş sağlayabiliyoruz. Başka bir mail servis için sistemi uyarlamak da oldukça kolay.
Örneğin Yandex email servis için gerekli servis yazılır.
public class YandexMailService : IMailService
{
public void SendMail(Mail mail)
{
Console.WriteLine($"yandex mail service/send mail\n{mail.Reciever}\n{mail.Content}");
}
}
Ve tek değişiklik ile sistem yandex mail servisi kullanmaya başlar.
var orderController = new OrderController(new YandexMailService());
orderController.Post();
Kodun çıktısı:
yandex mail service/send mail
reciever@gmail.com
an email content
IoC Container
Mail servis yönetimini esnek hale getirdik, şimdi ise bağımlılık yönetimini kolaylaştırmak adına inversion of control yapısını nasıl kullanabileceğimize bakacağız.
Bir konteynır yapısı kurmalıyız. Tüm soyutlamalar ve bu soyutlamaları uygulayacak sınıfları bu konteynır içerisinde eşleştirmeliyiz. Herhangi bir zamanda bir soyutlamayı kullanacağımız zaman konteynırdan eşlendiği sınıfı çağırmalı ve o nesne üzerinden işlemlerimize devam etmeliyiz, şöyle ki:
Konteynır
Bağımlılık tablosunu bir konteynır içerisinde tutacağız.
public class SimpleContainer
{
}
Hangi soyutlamaya hangi sınıftan nesne üretilmesini istediğimizi burada saklamamız gerekiyor. Bunun için HashMap veri yapısını kullanacağız. C# içerisinde Dictionary bu veri yapısını uygulamamızı sağlıyor. Burada tutacağımız değerler hangi soyut tipe hangi somut sınıfın denk geldiğini tutmak.
public class SimpleContainer
{
private readonly Dictionary<Type, Type> _map = new Dictionary<Type, Type>();
}
Bağımlılık tablosuna yeni bir değer eklemek için Register metodu oluşturuyoruz.
public class SimpleContainer
{
private readonly Dictionary<Type, Type> _map = new Dictionary<Type, Type>();
public void Register<TAbstraction, TImplementation>()
{
_map[typeof(TAbstraction)] = typeof(TImplementation);
}
}
Bağımlılık tablosundan bir soyutlamaya karşılık gelen sınıftan bir nesne elde etmek için Resolve metodunu oluşturuyoruz.
public TAbstraction Resolve<TAbstraction>()
{
Type type = _map[typeof(TAbstraction)];
return (TAbstraction)Activator.CreateInstance(type);
}
Artık bağımlılıkları tek noktadan yönetmek için new anahtar kelimesini kullanmayacağız ve tüm bağımlılıkları SimpleContainer’a ekleyip SimpleContainer’dan isteyerek çözüyoruz.
SimpleContainer container = new SimpleContainer();
container.Register<IMailService, YandexMailService>();
IMailService service = container.Resolve<IMailService>();
var orderController = new OrderController(service);
orderController.Post();
Artık konteynırda eşleşmiş olan servislerden ihtiyacımız olan servisi alıp Sipariş Yönetim sınıfına enjekte ediyor. (kurucu metoduna parametre olarak veriyor)
Başka bir mail servisini kullanmak istediğimizde ise sadece konteynırdaki map tablosuna yeni eşleşmeyi kaydetmemiz gerekiyor.
container.Register<IMailService, GmailService>();
Kod çıktısı:
Gmail Service sent an email.
sender:sender
reciever:reciever@gmail.com
content:an email content
Okuduğunuz için teşekkür ederim.