İlkay İlknur

Dependency Injection Nedir ? - C# ile Örnek Uygulama

Ekim 04, 2011

Merhaba Arkadaşlar,

Yazılım geliştirme süreçleri içerisinde en önemli kısımlardan biri de yazılımın tasarım sürecidir. Genelde ülkemizde her ne kadar tasarım yapılmadan doğrudan (bodoslama) :) kodlama kısmına geçilse de kodun ilk yazılması sırasında yapılmayan tasarımın acısı genelde ilerleyen süreçlerde ürünler geliştikçe, yeni istekler geldikçe ortaya çıkmakta.

Yazılım tasarım presiplerinden biri de yazılım içerisinde bulunan componentlerin “loosely coupled”(gevşek bağlı) olmasıdır. Bu şekilde yapılan yazılım tasarımlarında yapılar birbirine sıkı sıkıya bağlı olmadığından dolayı gelebilecek olan yeni taleplerde uygulamalar içerisinde yapılacak olan değişiklikler de minimuma inecektir.

Şimdi gelin basit bir uygulama üzerinde bir yazılım içerisindeki bağımlılıkları inceleyelim.

Örnek uygulamamız içerisinde akışlarımız sırasında loglama ve mesaj gönderme kısımlarının olduğunu düşünelim ve şu şekilde ilgili işlemlerden sorumlu olan sınıflarımız olduğunu varsayalım.

Database loglaması İçin Kullanılabilecek Olan DBLogger Sınıfı

class DBLogger
{
    public void WriteLog(string message)
    {
        Console.WriteLine(String.Format("DBLogger : {0}", message));
    }
}

Mail Gönderimi İçin Kullanılabilecek Olan MailSender Sınıfı

class MailSender
{
    public void SendMessage(string message)
    {
        Console.WriteLine(String.Format("MailSender : {0}", message));
    }
}

Ayrıca hayal ettiğimiz uygulamamızın ana akışını içerisinde bulunduran Processor isimli sınıfı da şimdilik şu şekilde geliştirebiliriz.

class Processor
{
    public void Process()
    {
        DBLogger logger = new DBLogger();
        logger.WriteLog("Log Text");
        //Ana Uygulama Akışı          
        MailSender messageSender = new MailSender();
        messageSender.SendMessage("Message Text");
    }
}

Son olarak uygulamamızı basit bir console uygulaması olarak çalıştıracağımız için yazacağımız basit çağrım ise şu şekilde olacak.

static void Main(string[] args)
{
    Processor processor = new Processor();
    processor.Process();
}

Evet, şimdilik kod kısmının sonuna geldik. Kısaca simule ettğimiz senaryodan bahsedersek, processor isimli sınıfımız bizim business(iş) katmanı dediğimiz görevi üstlenerek uygulamanın ana akışını yönetmekten sorumlu. Uygulama akışı içerisinde ise öncelikle DBLogger sınıfı kullanılara database’e loglama yapılmakta, daha sonra ise uygulama akışı bittikten sonra MailSender sınıfı kullanılarak Mail gönderimi yapılmakta. Uygulamayı çalıştırdıktan sonra şu şekilde bir ekran görüntüsü elde edeceğiz.

Buraya kadar olan kısımda klasik bir biçimde tüm isteklerin sabit olduğunu varsayarak uygulamamızı geliştirdik. Peki uygulamamızı başka bir kuruma verdiğimizi düşünelim ve bu kurumun loglama için dosya sistemini kullandığını ve Mail yoluyla mesajları almak istemediğini ve SMS sistemini tercih ettiğini düşünelim.

Öncelikli olarak yapacağımız şey tabi ki de dosyaya loglama yapacak ve mesajları SMS yoluyla gönderecek olan bileşenleri geliştirmek.
class FileLogger
{
    public void WriteLog(string message)
    {
        Console.WriteLine(String.Format("FileLogger : {0}", message));
    }
}
class SMSSender
{
    public void SendMessage(string message)
    {
        Console.WriteLine(String.Format("SMSSender : {0}", message));
    }
}

Evet, belirtilen isteklere uygun olarak geliştirmelerimizi tamamladık. Şimdi sıra geldi bu kısımları iş akışımız içerisine yerleştirmeye.

class Processor
{
    public void Process()
    {
        //DBLogger logger = new DBLogger();                   
        FileLogger logger = new FileLogger();
        logger.WriteLog("Log Text");
        //Ana Uygulama Akışı          
        //MailSender messageSender = new MailSender();
        SMSSender messageSender = new SMSSender();
        messageSender.SendMessage("Message Text");
    }
}

İş akışımız içerisinde database’e loglama yapan kısmı ve Mail gönderen kısmı commentleyerek yeni geliştirmiş olduğumuz bölümleri uygulamaya ekleyerek uygulamamızı çalıştırdık ve ihtiyaçları karşıladık. Peki başka bir kurum da dosya loglaması ve Mail ile mesajlaşma isterse yine gereken kısımları commentleyip uygulamamızı tekrardan mı build edeceğiz !!!!

Hımmm… Sanki birşeyler yanlış gidiyor. Kullanıcıların istekleri değişiklik gösteriyor ve biz anında bu isteklere adapte olamıyoruz. Peki neyi yanlış yapıyoruz !!!

Uygulama Bağımlılıkları ve Dependency Injection

Yukarıda yaptığımız uygulamaya baktığımızda uygulamamızın bir takım sınıflara sıkı sıkıya bağlı olduğunu görmekteyiz. Yani uygulamamızın akışını radikal değişiklikler yapmadan değiştiremiyoruz. Bu da uygulamamızın genişletilebilir olmasını çokta fazla mümkün hale getirmiyor.

Peki ne yapabiliriz ?

Baktığımız zaman aslında farklı akışlarımız olsa da bu akışlar temelde bir işi farklı bir biçimde gerçekleştirmekte. Biri mesaj gönderme işini SMS veya E-Mail yoluyla yaparken diğeri loglama işlemini database veya dosya olmak üzere farklı lokasyonlar üzerinde yapmakta. Bu noktada aklımıza gelen ilk iyileştirme bu işlemleri bir interface üzerinden yönetmek olmalı.

Öyleyse hemen uygulayalım.

interface IMessageSender
{
    void SendMessage(string Message);
}
interface ILogger
{
    void WriteLog(string message);
}
class DBLogger : ILogger
{
    public void WriteLog(string message)
    {
        Console.WriteLine(String.Format("DBLogger : {0}", message));
    }
}
class MailSender : IMessageSender
{
    public void SendMessage(string message)
    {
        Console.WriteLine(String.Format("MailSender : {0}", message));
    }
}
class FileLogger : ILogger
{
    public void WriteLog(string message)
    {
        Console.WriteLine(String.Format("FileLogger : {0}", message));
    }
}
class SMSSender : IMessageSender
{
    public void SendMessage(string message)
    {
        Console.WriteLine(String.Format("SMSSender : {0}", message));
    }
}

Gördüğünüz gibi uygulamamız içerisinde interface’leri kullanarak hiyerarşik bir yapı oluşturduk ve yeniden kullanımın mümkün olduğu bir yapı geliştirdik. Şimdi gelelim iş akışımızın bulunduğu Processor isimli sınıfımıza. Bu sınıf içerisinde iş akışımız yukarıdaki durumda daha önceden belirlenmiş bir şekilde gidiyordu. İşte şimdi tam bu noktada yarattığımız interface’ler sayesinde uygulamamızın akışını istediğimiz gibi dinamik olarak belirleyebiliriz.

class Processor
{
    ILogger logger = null;
    IMessageSender messageSender;
    public Processor(ILogger _logger, IMessageSender _messageSender)
    {
        logger = _logger;
        messageSender = _messageSender;
    }
    public void Process()
    {
        logger.WriteLog("Log Text");
        //Ana Uygulama Akışı                    
        messageSender.SendMessage("Message Text");
    }
}

Gördüğünüz gibi yukarıda ilgili loglama ve mesaj gönderme nesnelerin yaratılmasını farklı metotlara aktardık. Peki bu metotların içerisi nasıl olacak ? (Hangi tiplerin yaratılacağını basit bir şekilde config dosyasından alıyoruz)

static IMessageSender CreateMessageSender()
{
    IMessageSender retVal;
    string messageSender = ConfigurationManager.AppSettings["message"].ToString();
    if (messageSender == "SMS")
    {
        retVal = new SMSSender();
    }
    else
    {
        retVal = new MailSender();
    }
    return retVal;
}
static ILogger CreateLogger()
{
    ILogger retVal;
    string logger = ConfigurationManager.AppSettings["logger"].ToString();
    if (logger == "DB")
    {
        retVal = new DBLogger();
    }
    else
    {
        retVal = new FileLogger();
    }
    return retVal;
}

Şimdi sıra geldi akışın dinamik olarak yaratılmasına ve çalıştırılmasına. Yukarıda dikkat ettiyseniz Processor sınıfının constructor’ı içerisinde çalıştıracağı tipleri parametre olarak almakta. Yani biz eğer parametre olarak constructor’da belirtilen tipi implemente eden bir tipi geçirirsek uygulamamız otomatik olarak interface üzerindeki metodu çağırdığından dolayı uygulamamızın akışı doğrudan ilgili tipin içerisinde bulunan akışa doğru yönlenecektir.

static void Main(string[] args)
{
    Processor processor = new Processor(CreateLogger(), CreateMessageSender());
    processor.Process();
}

Evet arkadaşlar işte yukarıda uyguladığımız yapıya Dependency Injection diyoruz. Dependency Injection, Inversion of Control adını verdiğimiz uygulama akışını değiştirme yönteminin özelleşmiş bir halidir. Dependency Injection ile uygulamanın çalışacağı ve bağımlı olduğu akışları dışarıdan enjekte ederek uygulama akışını dinamik olarak değiştirebiliyoruz. Böylece uygulamamızın genişletilebilmesi ileri zamanlarda gelebilecek olan yeni geliştirmelerde uygulamamızın en az oranda etkilenmesini mümkün kılabilmekteyiz.

Bitirirken

Üniversite yıllarında(çok uzun değil sadece 1 sene önce :) ) aldığım yazılım tasarımı dersinde uygulama içerisindeki bağımlılıkları tanımlarken “bilmek” kelimesini kullanırdık. Yani ilk uygulamada baktığımızda Processor tipi DBLogger ve MailSender tiplerini biliyordu. Ancak son yaptığımız geliştirmeye bakarsanız Processor sadece temel interface’leri biliyor. Yani bu interface’leri implemente eden ister 100 ister 1000 adet tip olsun ve içlerinde yaptıkları akışlar farklı olsun bu durum artık kesinlikle processor sınıfını ilgilendirmiyor.

Evet arkadaşlar özellikle genişlemesi olası olan uygulamalar yazıyorsanız veya isteklerin çeşitleneceği noktalar üzerinde çalışıyorsanız Dependency Injection kullanmanızı mutlaka tavsiye ederim. Böylece en az acı ile uygulamalarınızda gerekli değişiklikleri gerçekleştirebilirsiniz. ;)

Hoşçakalın,

Not : Blog yazıları haricinde sosyal medya üzerinden anlık paylaşımlarımı Twitter ve Facebook'tan takip edebilirsiniz.