Dependency Inversion Principle (DIP)

Merhaba bu makalede sizlere nesneye yönelik yazılım tasarımının temel prensiplerinden olan Dependency Inversion Principle(DIP) hakkında bilgi verip ufak bir örnekler anlatmaya çalışacağım.Türkçe olarak ifade etmeye çalışırsak buna bağımlılığın ters çevrilmesi diyebiliriz.

Klasik prosedürel programlamada stilinde geliştirilen yazılımlarda genellikle üst seviyeli modüller, sınıflar vb.. alt seviyeli modüllere bağımlıdır. Bunun en önemli dezavantajlarından biriside alt seviyeli modüller ya da sınıflar daha sık değişebilir ve de bu değişimden de üst seviyeli modüller etkilenir. Bu değişimin sonucunda üst seviyeli modüllerde kod içerisinde değişiklik yapmak ve zincirmele olarak tüm modülleri derleyip yayınlamak gerekir. Sonuçta ufak değişikliklerden bile en üst seviyeye kadar etkilenebilen yönetilmesi zor bir model oluşur. Bu tarz modeller modüler olarak tekrar kullanılabilir gözüksede üst seviyeden alt seviyeye kadar bağımlılık zinciri olduğu için tekrar kullanmak istediğimiz modülle birlikte diğer modülleri de dahil etmemiz gerekir.Bu da bize gerçek anlamda bir tekrar kullanılabilirlik sağlamaz çünkü gerçek anlamda birbirinden soyutlanmış modüller yoktur. Malesef günümüzde modern birçok nesneye yönelik programlama dilinde dahi bu şekilde yazılım geliştirilmektedir çünkü kullandığımız programlama dilinin nesneye yönelik olması bizim nesneye yönelik yazılım geliştireceğimiz anlamına gelmez. Aşağıda örnek olarak prosedürel yöntem ve Dependency Inversion Principle(DIP) yöntemi ile tasarlanmış modelleri görebiliriz.

ProceduralLayers DIPLayer

Yazılım geliştirmede genellikle Tekrar Kullanılabilirlik ve Esneklik terimleri sıkça duyarız. Peki gerçekten bu terimler bize ne ifade eder? Tekrar kullanılabilir ve esnek yazılımları nasıl geliştirebiliriz ?Bu gibi soruların cevabı profosyonel yazılım geliştirmede oldukça önemlidir. Önce bağımlılık ve tekrar kullanılabilirlik ile başlayalım.

Bağımlılık teriminide kısaca özetlersek bir sınıfın başka bir sınıfa bağımlı olması yani bir sınıfın çalışması için başka bir sınıfa ihtiyaç duymasıdır diye açıklayabiliriz Yandaki şekilde gördüğümüz gibi üst seviyeli bir katman alt seviyeli katmanı (sınıfları,arayüzler… olabilir ) kullandığı için burada üst seviyeli katmanımız alt seviyeli katmana bağımlı diyebiliriz. Tekrar kullanılabilirlik terimine gelince; ilk olarak bir sınıfın yazılıp projenin çeşitli yerlerinde kullanılması aklımıza gelir fakat gerçek anlamda tekrar kullanım yazdığımız bir kod, üst seviyeli bir kütüphane diğer projelerde koduna dokunulmadan kullanılabiliyosa bunu tekrar kullanılabilir modül ya da yazılım olarak tanımlayabiliriz. Bunuda iyi tasarlanmamış yazılımlarda sınıfların birbirine olan bağımlılığı yüzünden genellikle yapamayız.

İşte bu noktada daha esnek ve yeniden kullanılabilir modüller geliştirebilmek için uygulanması gereken önemli bir yazılım prensibi olan Dependency Inversion Principle(DIP)(Bağımlılığın Ters çevrilmesi) devreye giriyor. Bu yöntemi kısaca açıklarsak;

Üst seviyeli modüller,sınıflar vb.. alt seviyeli sınıflar, modüllere bağımlı olmamalıdır. Alt seviyeli modüller üst seviyeli modüllere(modüllerin arayüzlerine) bağımlı olmalıdır. Buna kısaca Dependency Inversion (bağımlılığın ters çevrilmesi) deriz.

Bunu da şekilde gördüğümüz gibi üst seviyeli bir modül kendi arayüzünü tanımlayarak bu arayüzü kullanır. Bu arayüz abstract ya da interface sınıflar olabilir. Ardından alt seviyeli modüller üst seviyeli modülün arayüzünü uygular. Böylece üst seviyeli modülümüz alt seviyeli detaylarla ilgilenmeden onlardanki değişimlerden etkilenmeden kullanılabilir. Üst seviyeli modülümüz bir arayüze bağımlı olduğu için ve arayüzde genellikle değişmeyeceği için arayüzü gerçekleyen (implemention) sınıflar değişse dahi aynı kalacaktır. Günümüzdeki frameworklerin tasarlanmasında kullanılan temel prensiplerden biri budur.

Bu kadar laf kalabalığından sonra biraz sıkılmış olabilirsiniz.Ufak bir örnekle sıkıntınızı alıp işe koyulalım:) Örnek senaryomuz şöyle olsun:

Geliştirdiğimiz bir yazılımda işyerimizle alakalı kayıtları işliyoruz. Programımızın işyerindeki çalışanlara ait çeşitli formatlarda rapor sunması gerekiyor. Şuanda bizden istenen excel ve word formatından raporları sunması. Fakat ileride değişik türden formatta raporlarıda kolaylıkla sunması kesinlikle istenecektir. Unutmayın müşteri istekleri bitmez:) Bu işlemleri yaptığımız bir çalışan raporları sayfamız var. Bu sayfadan herhangi bir çalışanı seçip onla alakalı çeşitli formattaki raporları alabiliyoruz. Bu isteklere göre tasarladığımız sınıflarımızın UML diyagramını aşağıda görebiliriz.

Dependency

Yukarıdaki şekilde de gördüğümüz gibi Calisan rapor forumumuz CalisanRaporFormu sınıfı üst seviliyeli bir işi yapan sınıftır. CalisanWordRaporu ve CalisanExcelRaporu ise bu üst seviyeli işlemin nasıl yapılacağını değişik şekillerde uygulayan alt seviyeli detaylarla ilgilenen sınıflardır. CalisanRaporFormu bu sınıfları direkt olarak kullanır. Yani CalisanRaporFormu sınıfı alt seviyeli sınıflara direk olarak bağımlıdır onlarda meydana gelecek değişimlerden etkilenecek ve tekrar derlenmek zorunda kalacaktır. Bu şekilde tasarlanmış yazılımlığımızın kodları kabaca aşağıdaki gibi olur. Kodlara baktığımızda ilk olarak if else yapısı dikkatinizi çekmiştir. Genellikle kodunuzda buna benzer kısımları görürseniz büyük ihtimalle refactoring(yeniden biçimlendirme) yapmanızda fayda vardır. İyi şekilde tasarlanmış yazılımlarda genellikle if-else ve switch-case cümleleri kodun içinde fazla bulunmaz. Bunu da aklımızda bulundurmamızda fayda var. Aşağıda buna örnek bir dizi sınıf tasarımı verilmiştir.

public class CalisanRaporFormu {
        private List calisanlar;
        private Calisan seciliCalisan;
        private CalisanWordRaporu wordRaporu =new CalisanWordRaporu();
        private CalisanExcelRaporu excelRaporu =new CalisanExcelRaporu();

        public CalisanRaporFormu(){ }

        public void SeciliCalisanaAitRapor()
        {
                if(raporTipi=="Word")
                        wordRaporu.RaporOlustur(seciliCalisan);
                else if(raporTipi=="Word")
                        excelRaporu.RaporOlustur(seciliCalisan);
        }
}
public class Calisan {
        private string adi;
        private int maas;
        private string soyadi;
        private int mesaiSaati;
        public Sirket m_Sirket;

        public Calisan(){ }

        public int MesaiUcretiHesapla(){ }

        public string adi{
                get{return adi;}
                set{adi = value;}
         }

         public int maas{
                get{return maas;}
                set{maas = value;
         }
}
public class CalisanWordRaporu {
      public CalisanWordRaporu(){

      }

       public void RaporOlustur(Calisan calisan){
            //raporlama ile alakalı kodlar....
      }
}

public class CalisanExcelRaporu {
      public CalisanExcelRaporu(){

      }

       public void RaporOlustur(Calisan calisan){
            //raporlama ile alakalı kodlar....
      }
}

Yukarıdaki kodlar senoryamuzdaki bizden istenenleri gerçekleştiriyor. Müşterimide yazılımı teslim ettik herşey gayet güzel çalışıyor. Fakat beklenen kara gün geldi müşteri yeni isteklerle karşımıza çıktı:) Bunlardan senaryomuzda biraz da olsa bahsetmiştik.Bunlardan birtanesi senaryomuzda bizden istenen değişik formatta rapor çıktısı sunmasıydı ve müşteri bizden Pdf formatındaki raporları da sisteme eklememizi söyledi. Fakat bundan sonra yapılacak değişimler için önümüze birkaç problem çıkıyor. Yeni bir çalışan rapor formatını sisteme nasıl ekleriz? Bunun için eski kodlarımızın içini didiklemeye başladık ve bu işlemin CalisanRaporFormu sınıfı içinde yapıldığını gördük. Hemen kolları sıvadık başladık yeni rapor formatını sistemimize eklemeye.Sonra bütün kodlarımızı tekrar derleyip yeni halini müşterimize gönderdik.Değişin CalisanRaporFormu sınıfımız ve yeni eklenen Pdf raporu sınıfımız aşağıdaki şekilde olacaktır.

public class CalisanRaporFormu {
        private List calisanlar;
        private Calisan seciliCalisan;
        private CalisanWordRaporu wordRaporu =new CalisanWordRaporu();
        private CalisanExcelRaporu excelRaporu =new CalisanExcelRaporu();

        private CalisanPdfRaporu pdfRaporu =new CalisanPdfRaporu();

        public CalisanRaporFormu(){ }

        public void SeciliCalisanaAitRapor()
        {
                if(raporTipi=="Word")
                        wordRaporu.RaporOlustur(seciliCalisan);
                else if(raporTipi=="Word")
                        excelRaporu.RaporOlustur(seciliCalisan);
                else if(raporTipi=="Pdf")
                        pdfRaporu.RaporOlustur(seciliCalisan);
        }
}
public class CalisanPdfRaporu {
      public CalisanPdfRaporu(){

      }

       public void RaporOlustur(Calisan calisan){
            //raporlama ile alakalı kodlar....
      }
}

Yukarıda gördüğümüz gibi ufak bir değişiklik sistemin birçok yerinde değişiklik yapmamızı mecbur kılıyor ve bütün projeyi derleyip tekrar yayınlamamızı gerektiriyor. Kaldı ki müşterinin istekleri hiç bitmeyeceği için:) her yeni rapor formatında ya da programın değişik yerlerinde bu şekilde istekler için tekrar aynı işlemleri yapmak zorunda kalacağız en azından yukarıdaki gibi if-else cümleleri kodumuzu istila edecek bu da yazılımın yönetimini, yayınlanmasını, değişiklik eklemeyi ve yeniden kullanımız zorlaştıracak. Sistemimizi birde Dependency Inversion prensibine göre baştan tasarlayalım ve ardından bize sağladığı avantajlara bakalalım. Bunu yapmak için baştada bahsettiğimiz gibi bağımlılığı ters çevireceğiz. Yani CalisanRaporFormu sınıfımız alt raporlama sınıflarına bağımlı olmayacak kendi arayüzünü tanımlayacak ve yapacağı işlemlerde onu kullanacak. Ardından alt seviye sınıflar bu arayüzü uygulayarak CalisanRaporFormu sınıfımızın arayüzüne bağımlı olacak. Yeniden tasarlanmış sınıflarımızın UML diyagramı ve kodları aşağıdaki gibi olur.

DIPYeniden

public class CalisanPdfRaporu : ICalisanRaporu{
      public CalisanPdfRaporu(){

      }

       public void RaporOlustur(Calisan calisan){
            //raporlama ile alakalı kodlar....
      }
}

public class CalisanExcelRaporu : ICalisanRaporu{
      public CalisanExcelRaporu(){

      }

       public void RaporOlustur(Calisan calisan){
            //raporlama ile alakalı kodlar....
      }
}

public class CalisanWordRaporu : ICalisanRaporu{
      public CalisanWordRaporu(){

      }

       public void RaporOlustur(Calisan calisan){
            //raporlama ile alakalı kodlar....
      }
}
public class CalisanRaporFormu {
        private List calisanlar;
        private Calisan seciliCalisan;
        private ICalisanRaporu calisanRaporu;

        public CalisanRaporFormu(ICalisanRaporu calisanRaporu)
       {
               this.calisanRaporu = calisanRaporu;
       }

        public void SeciliCalisanaAitRapor()
        {
                this.calisanRaporu.RaporOlustur(seciliCalisan);
        }
}

Yukarıda yeni şekilde tasarladığımız kodlarda da gördüğümüz gibi artık üst seviyeli bir sınıfımız olan CalisanRaporFormu sınıfımız alt seviyeli sınıflardan olan CalisanPdfRaporu vb.. sınıflara bağımlı değildir, sadece kendi arayüzünü tanımlamış ve işlemlerinde bu arayüzü kullanmıştır. Alt seviyeli sınıflar ise CalisanRaporFormu arayüzü olan ICalisanRaporu arayüzünü CalisanRaporFormu sınıfına, onun kullandığı arayüze bağımlı olmuşlardır. Bu şekilde bağımlılığı ters çevirmiş olduk.Yukarıdaki kodları gördüğünüzde aklınıza CalisanRaporFormu sınıfının hangi rapor formatı ile çalışacağını nerden bileceği gelebilir. Bunu gördüğünüz gibi CalisanRaporFormu yapıcısında hangi rapor formatı ile çalıştığını alıyor. Genelde bu tür işlemler herbiri ayrı bir makale konusu olan Factory Tasarım Kalıbı ve Inversion Of Control (IoC) denilen tekniklerle yapılır. Şuanda kafanız o kısımda karışmasın diye deyinmek istemedim ayrı bir makalede bu konulara deyinmeye çalışacağım. Yeni şekilde tasarlanan kodun avantajlarına bakarsak gördüğümüz gibi CalisanRaporFormu artık kendi arayüzü uygulayan hangi tür rapor olursa olsun kod içinde hiçbir değişiklik yapmadan kullanabileceğiz. Tabi kodun içindeki if-else ifadeleri kalktığı için kodunda sadeleştiğini görüyoruz. Ayrıca bundan sonraki yeni rapor türleri için CalisanRaporFormu sınıfını tekrar derlemek zorunda değiliz çünkü bu sınıf ICalisanRaporu arayüzünü kullanıyor ve bu arayüz değişmedikçe tekrar dermemize gerek kalmayacak. Müşteriye sadece yeni eklediğimiz rapor formatı sınıflarını vermemiz yeterli olacaktır. Artık CalisanRaporFormu bizim için ayrı bir modül oldu. Değişik projelerde onun arayüzünü uygulayan herhangi bir sınıfla rahatlıkla çalışabilir. Tabi onu kütüphane yapıp aynı namespace içine ICalisanRaporu arayüzünüde eklemeyi unutmayalım. Paket yapımızda aşağıdaki gibi olur. Artık müşteride bulunan CalisanRaporu paketi(kütüphane,dll) yeni rapor formatı ekleme işlemleri için değişmeyecektir. Sadece RaporFormarlari paketine yeni rapor formatını ekleyerek tekrar derleyip müşteriye vermemiz yeterli olacaktır.

DIPPackage

Gördüğünüz gibi ufak bir örnekle de olsa önemli bir tasarım prensibinin bize sağladığı avantajları oldukça önemli. Yeniden kullanılabilir modüller ve framework’ler tasarlamanın temel prensiplerinden biri olan bu tasarım prenbini ufak bir örnekler inceledik umarım sizler için faydalı olmuştur. Tekrar görüşmek dileğiyle…

26 thoughts on “Dependency Inversion Principle (DIP)

  1. Barbaros

    Çok teşekkürler gerçekten. Interface gördüğüm ve okuduğum fakat proje içinde bilmediğimden dolayı uygulamaya kortuğum bir yapıydı. Bu yüzden makaleniz çok aydınlatıcı oldu. Fakat bir kaç sorum olacak.

    PDFReport dpf = new PDFReport(); // :ICreateReport
    ICreateReport h = dpf; // PDFReport sınıfından bir nesne oluşturup arayüze verdim
    CreateReportForm report = new CreateReportForm(h); // Bu nesne property olarak Calisan ve ICreateReport elemanı alıyor
    report.makeReport(); // PDF formatında rapot yapacağını artık biliyor ve raporu hazırlıyor

    Tam olarak hani tipte rapor vereceği böyle mi bildiriliyor ? Tam anlayamadım bu kısmı, aydınlatırsanız çok sevinirim.
    Çok teşekkürler

  2. M. Cihat Altuntaş Post author

    Evet interface’in güzelliği ve özelliğide bu zaten. Hangi tipte rapor vereceğini çalışma anında(run-time)’da anlıyor. Aslında sen ona PDFReport gönderdiğin zaman sistem ICreateReport’ın bir metodu çağrıldığında aslında PDFReport’un metodlarının çağırılması gerektiğini biliyor.Bir bakıma gerçek nesneyi gösteren bir C/C++’daki gibi bir Pointer olarak düşün.

  3. fatih

    merhaba,

    güzel bir yazı olmuş.Ancak aklıma takılan bir soru var.

    yukarıda “Müşteriye sadece yeni eklediğimiz rapor formatı sınıflarını vermemiz yeterli olacaktır.” dediniz. Aynı zamanda rogramın görsel kısmında da değişiklik yapılması gerekmezmi. Yani wordOluştur ve ExcelOluştur isimli butonlar varsa aynı zamanda PdfOluştur isimli bir buton da eklenmeli değilmi? ozaman yeni bir derleme gerekecek.

  4. M. Cihat Altuntaş Post author

    Ben olayı buton olarak değilde bir DropDownList üzerinden düşünerek yazmıştım. DropDownList üzerinden seçilen tip(Word,Pdf,Excel) ardından Rapor üret adında bir tek butonla yine dediğimi başarabiliriz. Bu “rapor üret” butonu altında şu şekilde bir kod olabilir.Olayın içine biraz Factory Pattern’da giriyor.

    public void RaporUret_Clicked(…)
    {
    ICalisanRaporu rapor =factory.CreateReport(ddlRaporTipi.SelectedItem.Text);
    rapor.Olustur(SeciliCalisan);
    }

    Bu şekilde olduğu zaman UI projesinin değişmesine gerek kalmaz.Aslında farklı butonlar koyarak da aynı şeyi yapabilirsin fakat biraz daha kompleks olur. Bunuda factory’de tanımlı rapor tiplerine göre otomatik olarak buton oluşturarak da yapabilirsin.

  5. koray

    Elinize sağlık çok faydalı bir makale olmuş.Benim de şöyle bir sorum olacak.
    şu şekilde bir ihtiyaç var. Her türlü dosya formatını okuyabilecek(csv,txt,xls) ve birden fazla yöntemle okuyabilecek(interop,ado,diger) bir component yazıyorum.
    Ancak sınıf tasarımını nasıl yapmam gerektiğini tam olarak kafamda kurgulayamadım.

    İlk önce şöyle düşündüm. Abstract bir FileReader sınıfı yapayım. Burada filePath bilgisini ve fileName bilgisini property olarak burada tutayım. Altında Bu sınıftan türeyecek CsvReader,XlsReader,TxtReader. Daha sonra ise Örnek XlsReaderdan da InteropXlsReader ve ADOXlsReader türeyecek.

    Dosya formatına göre CSV mi Xls Readerın mı yaratılacağına FileReaderFactory sınıfı belirleyecek. ve Kullanıcı tercihine göre de Interop ile veya ADO ile istediği dosya formatını istediği formatta okuyabilecek.

    İçinin kodu önemli değil sınıf tasarımı en iyi nasıl yapabilirim.Daha sonra yeni dosya formatı veya yeni okuma yöntemleri eklenebilir şekilde olmalı

  6. Barbaros

    “Yukarıdaki kodları gördüğünüzde aklınıza CalisanRaporFormu sınıfının hangi rapor formatı ile çalışacağını nerden bileceği gelebilir. Bunu gördüğünüz gibi CalisanRaporFormu yapıcısında hangi rapor formatı ile çalıştığını alıyor. Genelde bu tür işlemler herbiri ayrı bir makale konusu olan Factory Tasarım Kalıbı ve Inversion Of Control (IoC) denilen tekniklerle yapılır.”

    Merhaba,

    IoC hakkında bir makale yazmayı düşünüyor musunuz ? Yukarıda alıntı yaptığım örneğin nasıl çalıştığını anlatan bununla ilişkili bir IoC makalesi çok güzel olurdu. Umarım boş bir zamanınız olur ve bizim için güzel bir makale daha yazarsınız :)

    Teşekkürler

  7. M. Cihat Altuntaş Post author

    @Barbaros
    Aslında uzun zamandır IoC ile alakalı bir makale yazmak aklımda. Şu işler bir yoluna girip yoğunluktan kurtulabilsek en kısa zamanda yazıcam aklımda.

  8. Pingback: » Single Responsibility Principle(SRP)

  9. Pingback: Dumlupınar Üniversitesi ( DPÜ ) Bilgi ve Bilişim Kulübü » Blog Archive » Interface nedir,ne zaman kullanılır?

  10. Pingback: Interface nedir,ne zaman kullanılır? : Enes ONUR

  11. Ahmet Haşim

    Cihat Bey çok güzel bir makale yazmışsınız emeğinize,aklınıza sağlık. Teşekkürler.

  12. Furkan Ekinci

    Cihat hocam, ellerine sağlık. :) Tam aydınlanamadım henüz, daha çok okumam lazım ancak uzaktaki ışığı gördüm sayende. :)

  13. acemi yazılımcı

    Merhabalar,

    Neredeyse profosyonele yakın bir blog tuttugunuzdan ötürü size teşekkür ederim, pattern ve principle’ların bu kadar detaylı ve özenli olarak hazırlandıgı baska bir turkce bloga rastlamadım suana kadar. Blogunuz ilham verici ve ufuk acıcı.

    Konumuza gelince; DIP’i anlatırken gözden kacırmıs olabileceğiniz bir nokta oldugunu düşünüyorum, o da Open Close Principle. Base class olarak aldıgınız Calisan classının içerisindeki MesaiUcretiHesapla metodu, farklı calisan tiplerinde yapınızı çatlatacaktır, calısan class’ının abstract tanımlanıp, calısan tip classlarının abstract class’ı inherit ederek işlem yapması problemi cozecektir, UML diagrama baktıgımda ilk gözüme carpan sey buydu, küçük bir nüans, fakat dediğiniz gibi o kara gün gelip çattıgında hazır olmak adına bu önlemi almak gerekir diye düşünüyorum, iyi çalışmalar dilerim,
    Saygılarımla

  14. Özgür İlhan

    Okuduğum makaleler içinde (Türkçe, İngilizce), en iyilerinden biri diyebilirim. Prensip kullanımından önce ve sonrası örneği iyi olmuş. emeğinize sağlık.

Comments are closed.