Image
Eylül 16 2016 05:59

Interface Segregation Principle

            Bir önceki makalemde, Nesne Yönelimli Programlama ve Tasarımı için ortaya çıkmış ve 5 adet ilkeden oluşan SOLID prensiplerinden “S” ile gösterilen Single Responsibility Principle anlatmıştım.
Bu makalemde ise “I” ile gösterilen Interface Segregation Principle yani Arayüzlerin Ayrıştırılması ilkesini anlatmaya çalışacağım. Başka bir makelede SOLID hakkında detaylı bilgi yazmayı düşünüyorum, şimdilik bu adet ilkenin isimlerini yazarak konumuza dönelim. 

  • Single Responsibility (SRP) : Bir nesnenin sadece tek bir sorumluluğu olmalıdır.
  • Open Closed (OCP) : Nesne genişlemeye açık ama değişikliklere kapalı olmalı
  • Liskov Substitution Principle (LSP) : Temel sınıftan türeyen tüm alt sınıflar, birbirlerinin yerine kullanılabilir
  • Interface Segregation Principle (ISP) : Nesneler, ihtiyaç duymadıkları metotların bulunduğu Interface’lerden ayrıştırılmalı 
  • Dependency Inversion Principle (DIP) : Yüksek seviye modüller, düşük seviye modüllere bağlı olmamalı. Her ikisi de soyut (abstract) kavramlara bağlı olmalı.

Artık Arayüzlerin Ayrıştırılması Presibine (Kısaca ISP diyeceğiz) ye geçebiliriz. Nesneler ihtiyaç duymadıkları metodların bulunuğu interfacelerden arındırılmalıdır. Böylece nesnelerin birbirlerine bağımlılıkları azaltılarak daha uyumlu ve bakımı kolay olan projeler oluşturulması sağlanabilir. Dolayısı ile bir interface-ye yeni bir şeyler ekleneceği zaman durup "Bunun yer gerçekten burası mı olmalı ?" diye düşünülmelidir.
Birbiri ile ilişkisi olmayan birçok metottan oluşmuş büyük interfaceler yerine birbiri ile gerçekten ilişkili yapıları ihtiva eden birden fazla küçük interface-ler oluşturmak gerekir.

Peki, ISP uygulanmazsa ne olur? Birbirleriyle ilişkileri olmayan yapıları aynı interfacede tuttuğumuz için ilerde bu ayrık yapılara ilişkin yeni metodlarında eklenmesi kaçınılmaz olacak. Elimizde kendisini implemente eden classların bazı metodlarını kullandığı bazılarını kullanmadığı “ŞİŞMAN” ama gereksiz bir yapı olmuş olacak.

Daha önce yazdığımız yada öncekl yazılımcının ( :D ) yazdığı böyle bir “ŞİŞMAN” bir interfacemiz varsa ne yapmalıyız? 

  • Sadece ihtiyacımız olan yapıları belirleyip, bunları da birbirlerine ilişkileri ve bağımlılıklarıne göre gruplayıp gerekirse birden fazla interface oluşturmalıyız
  • Şişman interface kullandığımız yerlere, yeni interfacelerimizi implemente etmeliyiz
  • Bundan sonraki kullanımızda artık Şişman olanı değil yeni interface-mizi yada interfaceleri referans vermeliyiz. 

Kodlarımızın içinde böyle bir “ŞİŞMAN“ interface bulduk, değiştirmek istiyoruz fakat bu interface bizim olmadığı için müdahale edemiyorsak? Ancak kodlarımızın içinde de bu interface-in implemente edilmesinden kaynaklanan zorunlu ama bazı sınıflarda kullanıp bazılarında kullanılmayan yada hiç kullanılmayan metodlar varsa ne yapmalıyız?

  • Kodlarımızda gerçekten ihtiyaç duyduğumuz metodları barındıran daha küçük interface yada interfaceler oluşturmalıyız
  • Adapter Design Pattern ile kendi interfacelerinizi kullanarak “ŞİŞMAN” i besleyebiliriz. (Adapter Design Pattern ile ilgili makalemi okumanızı tavsiye ederim)

 

Meşhur printer örneğini biraz değiştirip ISP olmadan ve ISP uygulandıktan sonraki durumlarını inceleyelim. 

Senaryo:
Printer üreten bir firmada developer olarak çalışıyoruz. Firmamız yeni bir ürün çıkardı “All in One”  modelinde, Print, Fax, Scan, Photocopy yapabilen bir printer. Bizden bu  Printer için “Print, Fax, Scan, Photocopy” işlemlerini Client in kullanabileceği bir yapı oluşturmamız istendi. 

public interface IMachine
    {
        bool Print(List<Document> documents);
        bool Fax(List<Document> documents);
        bool Scan(List<Document> documents);
        bool PhotoCopy(List<Document> documents);
    }

    public class Document
    {
        // üzerinde işlem yapılacak belge
    }

Bizde yukarıdaki gibi bir Interface yazmaya karar verdik. “All in One” printerini kullanmak isteyen clientelere bu Interface-i verdik.

Sorunlar:

  1. Kodumuz SRP'ye uygun değil. Her sınıfın tek bir sorumluluğu olmalı. Bu Interface-i implemente eden classlar, Yazdırma, Fax, Tarama ve Fotokopi işlerini yapıyor olacak. Dolayısı ile herhangi birinde oluşacak bir değişiklik için bu Classı değiştirmek gerekecek.
  2. Client, Yazdırma, Fax, Tarama ve Fotokopi işlerinden sadece bir tanesi ile ilgileniyorsa ne olacak? O'nu yinede diğer 3 metodu implemente etmek zorunda bırakmış olacağız.
  3. Client tarafında sadece FAX işleminin yapılacağını varsayalım. Bu durumda, printerin kullanımı işi ile ilgili yazılımcı implemente etmek zorunda bırakıldığı ancak metodların içinin boş bırakıldığı büyük ihtimalle aşağıdaki gibi 
 public bool Print(List<Document> documents)
        {
            throw new NotImplementedException();
        }

        public bool Scan(List<Document> documents)
        {
            throw new NotImplementedException();
        }

        public bool PhotoCopy(List<Document> documents)
        {
            throw new NotImplementedException();
        }

         implemente edilmedi şekline hata dönecek bir yapı oluşturacak.
        Bu developerımız, Print,Scan ve PhotoCopy metodları kullanılmasın diye araya yukarda anlattığım gibi kendi interfacesini yazacak (sadece Fax ın kullanıldığı) Adapter pattern ilede onu IMachine ye çevirip kullanmak zorunda kalacak.

Peki, bu yapıyı ISP ye uygun bir hale getirmek için neler yapılması gerektiğine göz atalım.

Nesnelerin bağımlılıklarını kaldırıp birbiri ile ilgili olmayan metodları ayrı interfacelere taşımak gerekir. Bu yüzden önce bu adımları ayırıp her biri için interface oluşturalım.

    public interface IPrinter
    {
          bool PrintDocuments(List<Document> documents);
    }

    public interface IFax
    {
          bool FaxDocuments(List<Document> documents);
    }

    public interface IScan
    {
          bool ScanDocuments(List<Document> documents);
    }

    public interface IPhotoCopy
    {
        bool PhotoCopyDocuments(List<Document> documents);
    }

Bu interfaceleri implemente eden sınıfları ekleyelim. 

  public class Printer : IPrinter
    {
        public bool PrintDocuments(List<Document> documents)
        {
            Console.WriteLine($"{documents.Count()}  belge yazdırıldı.");
            return true;
        }
    }

    public class Fax : IFax
    {
        public bool FaxDocuments(List<Document> documents)
        {
            Console.WriteLine($"{documents.Count()}  belge fax yapıldı.");
            return true;
        }
        
    }

    public class Scan : IScan
    {
        public bool ScanDocuments(List<Document> documents)
        {
            Console.WriteLine($"{documents.Count()}  belge tarandı.");
            return true;
        }
    }


    public class PhotoCopy : IPhotoCopy
    {
        public bool PhotoCopyDocuments(List<Document> documents)
        {
            Console.WriteLine($"{documents.Count()}  belgenin fotokopisi çekildi.");
           return true;
        }
    }

Görüldüğü gibi artık istenilen "All in One" özelliği kullanılabilir. IPrinter, IFax, IScan ve IPhotoCopy interfacelerinden hangisini isterse kullanabilir client . Sadece printer özelliğini kullanmak isterse.

            var docs = new List<Document>
            {
                new Document{Content = "deneme",Id = 1},
                new Document {Id = 2,Content = "deneme2"}
            };

            var printer = new Printer();
            printer.PrintDocuments(docs);

şeklinde kullanması yeterli olacaktır. Diğer özellikleride bunun gibi tanımlayarak kullanabilir.

Benzer bir şekilde, eğer "All in One" özelliklerinin tamamı kullanılmak istenirse. IMachine interface ne bu Interfaceler referans olarak eklenerek kullanılabilir.

    public class Machine : IMachine
    {
        private readonly IPrinter _printer;
        private readonly IFax _fax;
        private readonly IScan _scan;
        private readonly IPhotoCopy _photoCopy;


        public Machine(IPrinter printer, IFax fax, IScan scan, IPhotoCopy photoCopy)
        {
            _printer = printer;
            _fax = fax;
            _scan = scan;
            _photoCopy = photoCopy;
        }

        public bool PrintDocuments(List<Document> documents)
        {
            return _printer.PrintDocuments(documents);
        }

        public bool FaxDocuments(List<Document> documents)
        {
            return _fax.FaxDocuments(documents);
        }

        public bool ScanDocuments(List<Document> documents)
        {
            return _scan.ScanDocuments(documents);
        }

        public bool PhotoCopyDocuments(List<Document> documents)
        {
            return _photoCopy.PhotoCopyDocuments(documents);
        }
    }

Ek bilgi olarak, Machine sınıfı oluştuğunda constructor-unda istenen nesnelerin otomatik eklenmesi sağlanabilir. Bunun için Dependency Injection Pattern e bakmakta fayda var. 
Normal kullanım ile DIP yapmak arasında nasıl bir fark olduğuna bakacak olursak.

Normal kullanım: 

 var allInOneMachine = new Machine(new Printer(), new Fax(), new Scan(), new PhotoCopy());
            allInOneMachine.FaxDocuments(docs);
            allInOneMachine.PrintDocuments(docs);
            allInOneMachine.PhotoCopyDocuments(docs);
            allInOneMachine.ScanDocuments(docs);

DIP ile kullanım  (Ben Autofac kullandım, başka DI toolları (Castle Windsor, Spring.net, Unity, Ninject vb.) da kullanbilirsiniz yada kendi DI toolunuzu yazabilirsiniz. Autofac tarnımda ben IMachine interface sine sahip bir nesne istediğim zaman, oluşan nesneye Printer, Fax, Scan ve Photocopy nesnelerini ekleyerek gönder demiş oluyorum.
Aşağıdaki yapıda Autofac containerinden IMachine interfacesine sahip bir class (yani Machine) ver demem yetiyor.

//Autofact kullanımı
// Yeni bir Machine nesnesi istiyorum
var machine = container.Resolve<IMachine>();
machine.FaxDocuments(docs);
machine.PrintDocuments(docs);
machine.PhotoCopyDocuments(docs);
machine.ScanDocuments(docs);

şeklinde kullanılabilir.

2016 : memet tayanç