Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

C# Composite Design Pattern(Composite Tasarım Deseni)

Merhaba,

Bu içeriğimizde Yapısal Tasarım Kalıplarından(Structural Patterns) olan Composite Tasarım Desenini(Composite Design Pattern) tam teferruatlı inceleyecek ve bol bol örnekler ile zihinlerimize mantıksal yapısını kazımaya çalışacağız. O halde fazla vakit kaybetmeksizin buyrun başlayalım…

Başlarken

Her design pattern makalesinde yaptığımız gibi öncelikle konuya dair bol teorik bilgi verdikten sonra bilgileri pratikselleştirmek için bir örnek üzerinden eş zamanlı anlatım sergileyeceğiz. Ardından Composite pattern’ın stratejik ve mantıksal yapısını daha da özümseyebilmek ve kullanılabileceği durumları hızlıca kavrayabilmek için beş adet örnek senaryo üzerinden pratik gerçekleştiriyor olacağız. Bu senaryoların her biri, çözümü ile birlikte makalemizin altındaki diğer sayfalarda sırasıyla sunuluyor olacaktır. Şimdiden iyi ve verimli okumalar dilerim…

Composite Design Pattern Nedir?

Bazen yazılım tasarımları süreçlerinde nesnelerin ağaç yapıları halinde oluşturulması ve ardından bu yapılarla tek tek nesnelermiş gibi çalışılması gerekebilir. İşte böyle durumlarda Composite pattern tasarımsal açıdan imdadınıza yetişebilir.

Composite pattern, kendi içlerinde birbirlerinden farklı olan bir grup nesnenin sanki tek bir bütün nesneymiş gibi kullanılmasını sağlamaktadır. Böylece farklı nesneleri bir ağaç yapısında birleştirip genel anlamda parça bütün ilişkisini yeniden düzenleyip şekillendirmektedir.

Composite pattern’a teoride şöyle bir örnek verebiliriz.
C# Composite Design Pattern(Composite Tasarım Deseni)‘Kutu’ ve ‘Ürün’ olmak üzere iki türlü nesne olduğunu düşünelim. Kutu nesnesi, içerisinde birden fazla ürün barındırabileceği gibi ayriyeten kutu da barındırabilir. Yani bir kutu içerisinde bir başka kutu olabilir. Aynı şekilde kutu içerisindeki kutu da içerisinde ürün barındırabileceği gibi yine farklı bir kutu da barındırabilir. Haliyle bu silsile böyle devam edebilir.
C# Composite Design Pattern(Composite Tasarım Deseni)Bu nesnelerin kullanıldığı bir sipariş sistemi oluşturduğumuzu tahayyül edelim ve yandaki görüntüdeki gibi bir sepeti düşünelim. Sepet içerisinde siparişler basit ürünlerden ibaret olabileceği gibi içi ürünlerle dolu kutulardan da ibaret olabilir. İşte böyle bir sipariş durumunda siz olsanız siparişin toplam fiyatını nasıl belirlerdiniz?

Muhtemelen çözüm olarak tüm kutuları tek tek açıp, mevcut tüm ürünleri gözden geçirdikten sonra toplam fiyatı hesaplardınız. Bu gerçek dünyada yapılabilitesi olan yegane yoldur diyebiliriz. Ancak yazılımsal olarak bu hesaplamayı yapabilmek pekte kolay değildir. Nihayetinde iç içe kaç kutu olduğunu bilemediğimiz için recursive bir yaklaşım sergilememiz gerekmektedir.

Composite pattern ise bu tarz ağaç yapısı senaryolarında gerçekleştirilecek bütünsel işlemleri yürütebilmek için her bir composite(kutu) nesnesi üzerinde operasyonu gerçekleştirmektedir. Yukarıdaki teorik örnekte olduğu gibi, toplam fiyatı hesaplayabilmek için her bir kutunun içerdiği ürünler üzerinden toplam fiyatı hesaplayarak öğrenecek ve bunu iç içe olan tüm kutular için gerçekleştirecektir.C# Composite Design Pattern(Composite Tasarım Deseni)Bu yaklaşımın en büyük yararı, ağaç yapısını oluşturan alt kırılım sınıflarıyla ilgilenmemize gerek olmamasıdır. Yani bir nesnenin basit bir ürün mü yoksa içinde birden fazla ürün ve kutu barındıran karmaşık bir kutu mu olup olmadığını bilmemize gerek yoktur. Ortak bir arayüz aracılığıyla her iki nesnede aynı muameleyi görecektir. Bu arayüz sayesinde tetiklenen metot üzerinden nesneler bu tetiklenmeyi ağaçtan aşağı ileterek operasyonun yürütülmesini sağlayacaktır.

Gerçek Dünya Analojisi

C# Composite Design Pattern(Composite Tasarım Deseni)Biliyorsunuz ki, çoğu ülkenin orduları hiyerarşik olarak yapılandırılmıştır. Bir ordu birbirleriyle ilişkili birkaç bölümden meydana gelmektedir. Misal olarak, bir tümen tugaylardan oluşurken, bir tugay alaylardan oluşmakta, alaylar ise mangalara kadar ayrılan müfrezelerden meydana gelmektedir. Haliyle bu şekilde ilişkilendirilmiş bir yapılanmada hiyerarşinin en üstünden verilen emirler, her seviyedeki tüm askerlere ulaştırılacak şekilde aktarılmaktadır. İşte bu hiyerarşinin düzenlenmesinde Composite pattern rahatlıkla kullanılabilir.

C# Composite Design Pattern(Composite Tasarım Deseni)Benzer mantıkla çalıştığınız şirkette veya kurumda ekibin organizasyonel yapısını ve personellerin pozisyonlarını işlevsel ilişkiler üzerinden hiyerarşik olarak gösteren teşkilat şemaları mevcuttur. Bu teşkilat şemaları da tıpkı ordularda olduğu gibi hiyerarşinin en üstünden altına doğru sorumluluk ağlarını göstermektedir. İşlevsel açıdan sorumluluklar, en üstten aşağı doğru her seviyedeki personellere ayrıştırılacak şekilde tasarlanmaktadır. Buradaki mantıkta yazılımsal düzleme Composite pattern ile rahatlıkla aktarılabilir.

Composite Design Pattern’ın Stratejisi

Composite pattern’ın uygulanacağı ağaç yapıları misali tasarımlarda Component, Composite ve Leaf isimlerinde olmak üzere üç farklı sorumlulukta sınıf tasarlanması gerekmektedir.
C# Composite Design Pattern(Composite Tasarım Deseni)Bu sınıfların ne olduğunu ve hangi sorumlulukları üstlendiğini açıklamamız gerekirse eğer;

  • Component
    Ağaç yapısındaki basit ve karmaşık nesneleri ve bu nesnelerin ortak alanlarını açıklayan abstract sınıftır.
  • Composite
    Ağaç yapısındaki karmaşık nesnelere karşılık gelen sınıftır. Daha teknik bir izahatte bulunmamız gerekirse eğer Component
    ‘lerin bir araya geldiği ve ağaç yapısındaki alt kırılımları oluşturan kompleks nesneleri temsil etmektedir.
  • Leaf
    Ağaç yapısındaki en temel unsuru olan ve alt kırılım barındırmayan tek bir Component nesnesidir. Yani basit nesneyi ifade eder.
C# Composite Design Pattern(Composite Tasarım Deseni)

Composite Object ile Leaf Object arasındaki ilişkiyi güzel bir şekilde temsil eden görsel şema…

C# Composite Design Pattern(Composite Tasarım Deseni)

Composite Pattern stratejisinin katılımcıları yukarıda görüldüğü gibidir… Burada dikkat edilmesi gereken nokta; basit öğeler Leaf, karmaşık öğeler ise Composite olarak nitelendirilmektedir…

Composite pattern’da birden çoğa hiyerarşik yapılanma söz konusudur.

Teknik Olarak Composite Design Pattern

Composite pattern’ı teknik olarak inceleyebilmek için yukarıdaki satırlarda değindiğimiz gerçek dünya analojisindeki asker senaryosundan yola çıkalım ve pratiksel açıdan ilgili senaryonun örneğini gerçekleştirerek seyredelim.

SENARYO
Emir komuta sistemine sahip ordu modelini yazılımsal olarak inşa edelim ve üst rütbelerden gelen emirleri tüm seviyelerdeki askerlere iletelim.
ÇÖZÜM
  • Adım 1 (Component Soyut Sınıfının Tasarlanması)
    Her şeyden önce ağaç yapılanmasında Leaf ve Composite nesnelerimizin temelini teşkil edecek olan Component soyut sınıfını tasarlayarak başlayalım.

        //Component
        public abstract class Soldier
        {
            protected string _name;
            protected Rank _rank;
    
            protected Soldier(string name, Rank rank)
            {
                _name = name;
                _rank = rank;
            }
    
            public abstract void AddSoldier(Soldier soldier);
            public abstract void RemoveSoldier(Soldier soldier);
            public abstract void ExecuteOrder();
        }
    

    Yukarıdaki kod bloğunda bir asker’i ifade eden Component soyut sınıf tasarımı gerçekleştirilmiştir. Dikkat ederseniz askere dair isim(_name) ve rütbe(_rank) bilgileri constructor üzerinden edinilmiş ve sadece Composite nesnelerde kullanılmak üzere ‘AddSoldier’ ve ‘RemoveSoldier’ abstract fonksiyonlarıyla birlikte hem Composite hem de Leaf nesnelerde kullanılacak ‘ExecuteOrder’ abstract metodu tanımlanmıştır.

    Esasında, Composite nesneler için tanımlanmış olan fonksiyonlar Leaf nesnelere de override edilecek olsa dahi ilgili nesneler için anlamlı fonksiyonlar olmayacak, boş olarak kalacaktırlar. Haliyle yazımızın ileriki satırlarında bu durumun Interface Segregation Principle‘a aykırılık oluşturabileceğini görecek ve nasıl önlemler alabileceğimizin istişaresini ediyor olacağız. Ayrıca sonraki sayfalarda sunacağımız beş adet örnek senaryo üzerinden de bu riski ortadan kaldıracak olan yaklaşımı sürekli tatbik ederek, tecrübe etmiş olacağız.

    Bu arada yukarıdaki kodda bulunan rütbe(Rank) içeriği aşağıdaki gibi olacaktır;

        //Soldier Rank
        public enum Rank
        {
            General, //General
            Colonel, //Albay
            LieutenantColonel, //Yarbay
            Major, //Binbaşı
            Captain, //Yüzbaşı
            Lieutenant //Teğmen
        }
    
  • Adım 2 (Composite Sınıfının Tasarlanması)
    Sıra ordu hiyerarşisinde alt kırılım noktalarına karşılık gelecek olan askerleri(komutan) oluşturmaya gelmiştir. Bu askerler emrinde bir veya birden fazla komutan veya asker barındıran görevliler olabilir.

        //Composite
        public class Commander : Soldier
        {
            List<Soldier> _soldiers = new();
            public Commander(string name, Rank rank) : base(name, rank)
            {
            }
    
            public override void AddSoldier(Soldier soldier)
                => _soldiers.Add(soldier);
    
            public override void RemoveSoldier(Soldier soldier)
                => _soldiers.Remove(soldier);
    
            public override void ExecuteOrder()
            {
                Console.WriteLine($"{_rank} - {_name}");
                foreach (Soldier soldier in _soldiers)
                    soldier.ExecuteOrder();
            }
        }
    

    Yukarıdaki kod bloğunu incelerseniz eğer ‘Commander’ sınıfı, emrinde birden fazla komutan ve asker barındıran(komutan seviyesinde) bir asker olduğu için ‘Soldier’ abstract class’ından türemektedir. Haliyle bu sınıf ilgili komutanın maiyetini temsil edecek olan 4. satırda tanımlanmış bir koleksiyon barındırmaktadır. Bu koleksiyon, ilgili askerin altındaki diğer askerleri temsil etmektedir. Dolayısıyla ilgili askerin maiyetine ‘AddSoldier’ ve ‘RemoveSoldier’ fonksiyonları eşliğinde askerler eklenmekte yahut varsa çıkarılmaktadır. Ayrıca ‘ExecuteOrder’ metoduna dikkat ederseniz eğer bu metot ile üstlerden gelen emirler doğrultusunda gerekli operasyonlar gerçekleştirilmekte ve içerisinde maiyeti üzerinde tek tek döngü ile işlemler gerçekleştirilerek hiyerarşik bir şekilde emri altındaki askerlere gelen emirler iletilmektedir.

  • Adım 3 (Leaf Sınıfının Tasarlanması)
    Ordu hiyerarşisinde, ağaç yapısının son halkası er olan askerlerdir. Haliyle bunlar tasarımımızda Leaf olarak nitelendirilmektedirler.

        //Leaf
        public class BuckPrivate : Soldier
        {
            public BuckPrivate(string name, Rank rank) : base(name, rank)
            {
            }
    
            //Bu fonksiyonun er olan askerler(Leaf) için bir anlamı yoktur!
            public override void AddSoldier(Soldier soldier)
                => throw new NotImplementedException();
    
            //Bu fonksiyonun er olan askerler(Leaf) için bir anlamı yoktur!
            public override void RemoveSoldier(Soldier soldier)
                => throw new NotImplementedException();
    
            public override void ExecuteOrder()
                => Console.WriteLine($"{_rank} - {_name}");
        }
    

    Yukarıdaki kod bloğuna göz atarsanız eğer ‘BuckPrivate’ sınıfı bir eri temsil ediyor olsa da özünde bir asker olduğu için yine ‘Soldier’ soyut sınıfından türemekte ve içerisinde gerekli implemantasyonları sağlamaktadır. Burada ilk dikkat edilmesini istediğim nokta ‘BuckPrivate’ın kendisinden sonra hiyerarşik açıdan başka bir askerin gelmediği, yani zincirin son halkası olduğudur. Haliyle bu nesne bir önceki ‘Commander’ sınıfında olduğu gibi bir koleksiyon vs. barındırmamaktadır. Bu sebepten dolayı komutanlardan gelen emirleri işleyen ‘ExecuteOrder’ metodu direkt o anki askere odaklı bir şekilde inşa edilmektedir.

    Bunların dışında ‘AddSoldier’ ve ‘RemoveSoldier’ fonksiyonları görüldüğü üzere er olan askerler için bir anlam ifade etmemektedir. Haliyle 1. adımda hafiften atıfta bulunduğumuz nokta burasıdır. Bu fonksiyonların, Leaf sınıfında işlevi olmadığı halde tanımlanmaları tasarımsal bir hatadan ziyade önceden de bahsedildiği üzere Arayüz Ayrım Prensibi‘ne aykırıdır.

    Peki bu duruma nasıl önlem alabiliriz?
    Bu duruma önlem alabilmenin yolu 1. adımda ki Component tasarımının arayüz ayrım prensibine uygun bir şekilde tasarlanmasından geçmektedir.

    Şöyle ki;

    Bir asker iki türlü olabilir. Ya emrinde farklı asker ve komutanlar barındıran bir asker ya da er! Dolayısıyla,

        //Component
        public abstract class Soldier
        {
            protected string _name;
            protected Rank _rank;
    
            protected Soldier(string name, Rank rank)
            {
                _name = name;
                _rank = rank;
            }
            public abstract void ExecuteOrder();
        }
    

    ‘Soldier’ sınıfı tüm askerlere genellenebilir şekilde yukarıdaki gibi tasarlanırken,

        //Component
        public abstract class SoldierComposite : Soldier
        {
            protected SoldierComposite(string name, Rank rank) : base(name, rank)
            {
            }
    
            public abstract void AddSoldier(Soldier soldier);
            public abstract void RemoveSoldier(Soldier soldier);
        }
    

    emrinde farklı asker ve komutanlar barındıran askerleri yukarıdaki gibi ‘SoldierComposite’ misali farklı bir sınıfta tasarlamak yeterli olacaktır. Nihayetinde bu iki sınıfta tasarımsal açıdan Component‘e karşılık gelecektir. ‘SoldierComposite’e göz atarsanız eğer Leaf için bir anlam ifade etmeyen tüm fonksiyonlar bu sınıf altında tanımlanmıştır.

    Tabi süreçte Composite ve Leaf sınıflarının da aşağıdaki gibi tasarlanması gerecektir.

        //Composite
        public class Commander : SoldierComposite
        {
            List<Soldier> _soldiers = new();
            public Commander(string name, Rank rank) : base(name, rank)
            {
            }
    
            public override void AddSoldier(Soldier soldier)
                => _soldiers.Add(soldier);
    
            public override void RemoveSoldier(Soldier soldier)
                => _soldiers.Remove(soldier);
    
            public override void ExecuteOrder()
            {
                Console.WriteLine($"{_rank} - {_name}");
                foreach (Soldier soldier in _soldiers)
                    soldier.ExecuteOrder();
            }
        }
    
        //Leaf
        public class BuckPrivate : Soldier
        {
            public BuckPrivate(string name, Rank rank) : base(name, rank)
            {
            }
    
            public override void ExecuteOrder()
                => Console.WriteLine($"{_rank} - {_name}");
        }
    

    Haliyle artık Leaf için anlamı olmayan fonksiyonlar lüzumsuz yere tanımlanmış olmayacaktır. Bundan sonra ilgili pattern’ın tatbik edilmesi amacıyla yazımızın devam sayfalarında vereceğimiz örneklerde direkt bu şekilde bir tasarım baz alınıp geliştirme yapılacaktır.

  • Adım 4 (Test)
    Yukarıdaki adımları tam teferruatlı bir şekilde gerçekleştirdiysek eğer artık Composite pattern inşası tamamlanmıştır demektir. Şimdi test edebiliriz.

            static void Main(string[] args)
            {
                //Composite general nesnes oluşturuyoruz.
                Commander general = new Commander("Gençay", Rank.General);
    
                //Composite general nesnesine Leaf nesneler ekliyoruz.
                general.AddSoldier(new BuckPrivate("Müslüm", Rank.Colonel));
                general.AddSoldier(new BuckPrivate("Orhan", Rank.Colonel));
    
                //Yeni Composite albay ve yarbay nesneleri oluşturuyoruz.
                Commander albay = new Commander("Hilmi", Rank.Major);
                Commander yarbay = new Commander("Cavit", Rank.LieutenantColonel);
    
                //Yarbay Composite nesnesine Leaf nesneler ekliyoruz.
                yarbay.AddSoldier(new BuckPrivate("Murat", Rank.Major));
                yarbay.AddSoldier(new BuckPrivate("Kaan", Rank.Major));
    
                //Albay Composite nesnesine yarbay nesnesini ekliyoruz.
                albay.AddSoldier(yarbay);
    
                //Albay Composite nesnesine Leaf nesne ekliyoruz.
                albay.AddSoldier(new BuckPrivate("Mustafa", Rank.LieutenantColonel));
    
                //Albay Composite nesnesini general nesnesine ekliyoruz.
                general.AddSoldier(albay);
    
                //General nesnesinden emri veriyoruz!
                general.ExecuteOrder();
            }
    

    Yukarıdaki kodu derleyip, çalıştırdığımızda aşağıdaki ekran görüntüsüyle karşılaşmaktayız.
    C# Composite Design Pattern(Composite Tasarım Deseni)İşte bu kadar… 🙂
    Görüldüğü üzere hiyerarşideki tüm nesneler tek tek tetiklenerek ordu yapılanmasında en başta verilen emir en alttaki askere kadar işletilerek, eriştirilmiştir.

Client açısından tekil nesneler ile nesne grupları arasındaki farklılıkları görmeksizin işlem yapmak için Composite pattern kullanılabilir.

Uygulanabilirlik

Yukarıda görüldüğü üzere Composite pattern, Leaf ve Composite yapılarından oluşan ve bu yapılar arasında stratejik bir ayrım sağlayan tasarım desenidir. İhtiyaç olarak ağaç yapısı(bir başka deyişle hiyerarşik) operasyonlarında özyinelemeli(recursive) nesne yapısı oluşturulmasına olanak sağlamaktadır. Bu recursive operasyon esnasında client, hem Leaf hem de Composite yapıları ekstra çabalara girişmeksizin aynı şekilde(fark yaratmaksızın) işlemektedir.

Lehte ve Aleyhte Durumlar
Karmaşık ağaç yapılarıyla rahatlıkla çalışılabilir. İşlevselliği çok farklı olan sınıflar için ortak bir arayüz sağlamak zor olabilir. Belirli senaryolarda Component arayüzünü aşırı genelleştirmek gerekebilir. Haliyle bu durumda ilgili tasarımın anlaşılmasını zorlaştırabilir.
Nesne ağacıyla çalışan nesneleri mevcut kodu bozmadan uygulamaya ekleyebildiğimizden dolayı Open Closed prensibiyle uyumludur. Composite pattern, yukarıdaki senaryonun 3. adımında bahsedildiği üzere Interface Segregation prensibine aykırı olabilir.
Daha az bellek yönetimi gerektirmektedir.
Esnektir.

İlkel(Leaf) ve karmaşık(Composite) nesnelerin işlenmesi farklı şekilde ele alınır. Haliyle her nesneyi işlemeden önce türünü sorgulama zahmetine katlanmaksızın Composite pattern ile hızlı çözüm getirebilirsiniz.

Diğer Kalıplarla İlişkisi
  • Karmaşık Composite ağaçlar oluştururken Builder Design Pattern kullanılabilir.
  • Chain of Responsibility, genellikle Composite pattern ile birlikte kullanılır. Bu durumda bir yaprak(Leaf) herhangi bir istek aldığında o isteği tüm ana bileşenlerin zincirinden geçirerek nesne ağacının köküne kadar iletebilir.
  • Composite ağacını tüm alt kırılımlarıyla birlikte tetikleyebilmek için Iterator Design Pattern kullanılabilir.
  • Composite ağacının tamamının üzerinde bütünsel işlem yapabilmek için Visitor Design Pattern kullanılabilir. Bununla ilgili, Visitor pattern için referans edilen ilgili makalemde Kritik başlığı altındaki yorumu okuyabilir ve örneği inceleyebilirsiniz.
  • Yoğun Composite pattern’ı kullanan tasarımlarda Prototype Design Pattern kullanılarak karmaşık yapıları sıfırdan oluşturma maliyeti ortadan kaldırılabilir.
Genel Görüşler

Composite pattern’ın tüm amacı birbirleriyle ilişkili karmaşık(composite) yapıların tıpkı bir yaprak gibi atomik olarak işlenebilmesidir. Nihayetinde bu kalıbın merkezinde, bir client’ın içinde bir çok nesne olduğunu bilmesine gerek kalmaksızın ilkel ve karmaşık nesneler(ağaç yapısı) üzerinde işlem yapabilme yeteği yatmaktadır.

Composite pattern ile heterojen olan ilkel(Leaf) ve karmaşık(Composite) nesne koleksiyonunu atomik olarak ele alabilmek için gerekli fonksiyonların Component arayüzünde tanımlanması yeterlidir. Ancak bu tanımlama, önceki satırlarda da değindiğimiz gibi(özellikle pratik örneğin 3. adımında) Leaf nesnelerinde de implemantasyona neden olacağından dolayı olası güvensizliğe mâl olabilir. Çünkü client’lar Leaf nesneler üzerinden Composite nesnelere özel olan fonksiyonları kullanmak gibi anlamsız eylemler yapabilir. Halbuki Component‘te tanımlı olan composite fonksiyonları Leaf nesneleri için bir anlam ifade etmeyecektir. Haliyle bu durum daha önceden de bahsedildiği gibi Arayüz Ayrım Prensibine aykırı olabilir. Bunu engellemek için Component‘i ikiye ayırıp composite fonksiyonlarını farklı bir arayüze alabilir ve Composite nesneleri o arayüzden türetebilirsiniz.

Evet, böylece bir design pattern makalesinin daha sonuna gelmiş olduk. Sonraki sayfalarda konuya dair farklı senaryolarda bol pratik örnekler sergilemekteyim. Kesinlikle göz atmanızı ve mümkünse teoride bırakmayıp pratik ile desteklemenizi tavsiye ederim.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.