C# Composite Design Pattern(Composite Tasarım Deseni)
Gençay
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. ‘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. 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.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
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.
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. 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.
Composite Object ile Leaf Object arasındaki ilişkiyi güzel bir şekilde temsil eden görsel şema…
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. İş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.
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…
SENARYO 2
Bir şirkette aşağıdaki hiyerarşide personel pozisyonları bulunmaktadır.
Proje Yöneticisi
Takım Lideri
Yazılım Geliştirici
Her bir pozisyonda bulunan personel, hiyerarşik olarak kendisinden sonra gelen herhangi bir pozisyondaki personeli maiyetinde barındırabilmektedir.
Bu senaryoda herhangi bir pozisyondaki personelin, maiyetindeki personellerin bilgilerini ve toplam maaşlarını hesaplayıp karşılığına yazdıralım.
ÇÖZÜM
Adım 1(Component Soyut Sınıfının Tasarlanması)
Composite pattern’ın tasarımında ilk olarak Leaf ve Composite nesnelerimizin ortak arayüzü olan Component abstract class’ı tasarlanarak başlanmaktadır. Haliyle bizler de vakit kaybetmeksizin ilk olarak ilgili sınıfı oluşturarak başlıyoruz.
//Component
public abstract class Employee
{
protected string _name;
protected double _salary;
protected Employee(string name, double salary)
{
_name = name;
_salary = salary;
}
public string GetName()
=> _name;
public double GetSalary()
=> _salary;
public abstract void Print();
}
Yukarıdaki ‘Employee’ adıyla tasarlanan Component sınıfı direkt Leaf ve Composite nesnelerinin kullanabileceği şekilde ortak member’larla tasarlanmıştır. Sadece Composite nesnelerine özel fonksiyonları barındıran Component tasarımı ise aşağıdaki olacaktır. Böylece hem Leaf nesnelerinde anlamsız member’lar olmayacak hem de arayüz ayrım prensibini sağlamış bulunacağız.
Ayrıca yukarıdaki Component örneğinde ‘GetName’ ve ‘GetSalary’ metotlarının gövdeleri direkt oluşturulmuşken, ‘Print’ metodunun gövdesi concrete olacak olan Composite ve Leaf nesnelerine bırakılmıştır. Nihayetinde bu kod, buradaki davranışın ihtiyaca göre tarafımızca belirlendiğine bir örnektir. Misal bizim concrete nesnelerde maaşlarla ilgili ekstradan işlemler yapmamız gerekseydi ‘GetSalary’ metodu abstract olarak ayarlayıp, bu işi ilgili nesnesinde inşaya bırakabilirdik. Ama gerekmediği için kıssadan hisse yapıp gerekli işlemi burada gerçekleştirmekteyiz.
//Component
public abstract class EmployeeComposite : Employee
{
protected List<Employee> _employees = new();
protected EmployeeComposite(string name, double salary) : base(name, salary)
{
}
public void AddEmployee(Employee employee)
=> _employees.Add(employee);
public void RemoveEmployee(Employee employee)
=> _employees.Remove(employee);
}
Composite nesneleri için özel olan fonksiyonları barındıran Component soyut sınıfımızda yukarıdaki gibi olacaktır. Burada da ‘AddEmployee’ ve ‘RemoveEmployee’ metotlarının gövdelerini aynı mantıkla concrete olan Composite nesnelerine bırakmaksızın direkt olarak inşa etmiş bulunmaktayız. Haliyle ilgili metotlar sadece ekleme ve çıkarma işlemleri yapacaklarından ve ekstra bir iş yürütmeyeceklerinden dolayı direkt bu ihtiyacı burada karşılamış bulunmaktayız.
Adım 2(Composite Sınıfının Tasarlanması)
Şimdi sıra, senaryo gereği altında en az bir pozisyondan personel barındıracak olan Composite sınıfını tasarlamaya gelmiş bulunmaktadır.
//Component
public class Manager : EmployeeComposite
{
public Manager(string name, double salary) : base(name, salary)
{
}
public override void Print()
{
Console.WriteLine($"Name\t\t:{GetName()}");
Console.WriteLine($"Salary\t\t:{GetSalary()}");
double totalSalary = 0;
_employees.ForEach(employee =>
{
totalSalary += employee.GetSalary();
employee.Print();
});
Console.WriteLine($"Total Salary\t:{totalSalary}");
}
}
Yukarıdaki ‘Manager’ isimli Component sınıfına göz atarsanız eğer ‘Print’ fonksiyonu ile ilgili personelin adını ve maaşını ekrana yazdırmakla birlikte altındaki elemanların da(ki bu elemanlar normal personel olabileceği gibi yönetici pozisyonunda bir personel de olabilir) toplam maaşını hesaplamakta ve ‘Print’ fonksiyonlarını tetikleyerek onlara dair bilgileri ekrana yazdırmaktadır.
Adım 3(Leaf Sınıfının Tasarlanması)
Şimdi ise altında herhangi bir pozisyondan personel barındırmayan personelleri temsil edecek olan Leaf sınıfını tasarlamaya sıra gelmiştir.
//Leaf
public class SoftwareDeveloper : Employee
{
public SoftwareDeveloper(string name, double salary) : base(name, salary)
{
}
public override void Print()
{
Console.WriteLine($"\tName\t\t:{GetName()}");
Console.WriteLine($"\tSalary\t\t:{GetSalary()}");
}
}
Yukarıdaki ‘SoftwareDeveloper’ isimli Leaf nesnesinde ise ‘Print’ fonksiyonuyla sadece ilgili personele dair adı ve maaş bilgileri ekrana yazdırılmaktadır.
Adım 4(Test)
Bu ana kadar dikkat ederseniz; ‘Proje Yöneticisi’ ve ‘Takım Lideri’ pozisyonları ‘Manager’ sınıfıyla, ‘Yazılım Geliştirici’ pozisyonu ise ‘SoftwareDeveloper’ sınıfıyla temsil edilmektedir. Artık yapılan bu tasarımı rahatlıkla test edebiliriz.
Yukarıdaki örnek kodu derleyip, çalıştırdığımızda aşağıdaki çıktıyı verecektir.
Görüldüğü üzere tüm pozisyonlardaki personellerin ve maiyetlerindeki diğer personellerin toplam maaşla birlikte bilgileri yazdırılmış bulunmaktadır.
İyi çalışmalar…
SENARYO 3
Bir uygulamadaki hiyerarşik menülerin Composite pattern ile tasarımını gerçekleştirelim.
ÇÖZÜM
Adım 1(Component Soyut Sınıfının Tasarlanması)
Bir menü, tek seviyelik bir menü olabileceği gibi(Leaf), altında birden fazla farklı menü barındıran karmaşık(Composite) bir menüde olabilir. Haliyle her iki türlüde ilgili menülerin ortak alanlarını barındıracak olan Component arayüzünü tasarlayalım.
//Component
abstract class Menu
{
protected string _name;
protected string _description;
protected Menu(string name, string description)
{
_name = name;
_description = description;
}
public string GetName()
=> _name;
public string GetDescription()
=> _description;
public abstract void Print(int level = 0);
}
//Component
abstract class MenuComposite : Menu
{
protected List<Menu> _menus = new();
public MenuComposite(string name, string description) : base(name, description)
{
}
public void AddMenu(Menu menu)
=> _menus.Add(menu);
public void RemoveMenu(Menu menu)
=> _menus.Remove(menu);
}
Yukarıdaki ‘Menu’ Component arayüzündeki ‘Print’ fonksiyonuna göz atarsanız eğer int türünden ‘level’ isminde bir parametre almaktadır. Bunun sebebi, ilgili menüleri listelerken derecelerine göre içe kıvrımlı bir şekilde görsellik oluşturabilmek içindir.
Adım 2(Composite Sınıfının Tasarlanması)
Ardından sırada Leaf ve Composite nesneleri oluşturmak vardır. Siz istediğinizden başlayabilirsiniz. Ben deniz Composite nesneyi tasarlayarak devam ediyorum.
//Composite
class MenuItems : MenuComposite
{
public MenuItems(string name, string description) : base(name, description)
{
}
public override void Print(int level = 0)
{
Console.WriteLine($"{(level == 0 ? "" : "\t".PadRight(level, '\t'))}{GetName()} {{{GetDescription()}}}");
foreach (Menu menu in _menus)
menu.Print(menu is MenuComposite ? level + 1 : level + 2);
}
}
Adım 3(Leaf Sınıfının Tasarlanması)
Benzer şekilde Leaf nesnesini de tasarlarsak eğer;
class MenuItem : Menu
{
public MenuItem(string name, string description) : base(name, description)
{
}
public override void Print(int level = 0)
=> Console.WriteLine($"{"\t".PadRight(level, '\t')}{GetName()} {{{GetDescription()}}}");
}
Adım 4(Test)
Evet, görüldüğü üzere tasarımsal inşa tamamlanmış bulunmaktadır. Şimdi sıra inşa edilen bu tasarımı test etmeye gelmiştir.
static void Main(string[] args)
{
MenuItems menuItems1 = new("Ana Menü 1", "Bu ana menü 1'dir...");
menuItems1.AddMenu(new MenuItem("Menü 1", "Bu alt menü 1'dir..."));
menuItems1.AddMenu(new MenuItem("Menü 2", "Bu alt menü 2'dir..."));
menuItems1.AddMenu(new MenuItem("Menü 3", "Bu alt menü 3'dir..."));
MenuItems menuItems2 = new("Ana Menü 2", "Bu ana menü 2'dir...");
menuItems2.AddMenu(new MenuItem("Menü 4", "Bu alt menü 4'dür..."));
MenuItems menuItems3 = new("Ana Menü 3", "Bu ana menü 3'dür...");
menuItems3.AddMenu(new MenuItem("Menü 5", "Bu alt menü 5'dir..."));
menuItems3.AddMenu(new MenuItem("Menü 6", "Bu alt menü 6'dir..."));
menuItems3.AddMenu(new MenuItem("Menü 7", "Bu alt menü 7'dir..."));
menuItems1.AddMenu(menuItems2);
menuItems1.AddMenu(menuItems3);
menuItems2.AddMenu(new MenuItem("Menü 8", "Bu alt menü 8'dir..."));
MenuItems menuItems4 = new("Ana Menü 4", "Bu ana menü 4'dür...");
menuItems4.AddMenu(new MenuItem("Menü 9", "Bu alt menü 9'dur..."));
menuItems4.AddMenu(new MenuItem("Menü 10", "Bu alt menü 10'dur..."));
menuItems4.AddMenu(new MenuItem("Menü 11", "Bu alt menü 11'dur..."));
MenuItems menuItems5 = new("Ana Menü 5", "Bu ana menü 5'dir...");
menuItems5.AddMenu(new MenuItem("Menü 12", "Bu alt menü 12'dir..."));
menuItems5.AddMenu(new MenuItem("Menü 13", "Bu alt menü 13'dir..."));
menuItems5.AddMenu(new MenuItem("Menü 14", "Bu alt menü 14'dir..."));
menuItems4.AddMenu(menuItems5);
menuItems3.AddMenu(menuItems4);
menuItems1.Print();
}
Yukarıdaki dolu dizgin menü verisiyle doldurulmuş örnek kod bloğunu derleyip, çalıştırırsak eğer aşağıdaki gibi bir ekran çıktısıyla karşılacağız. Görüldüğü üzere her menü seviyesine göre içe kıvrımlı bir şekilde menüler ekrana listeletilmiştir.
İyi çalışlar…
SENARYO 4
Bilgisayar ve bilgisayar parçaları satışı yapan bir mağazanın, bilgisayar toplama sürecini Composite pattern ile simüle edelim. Her bir bilgisayar parçasının fiyatını görebilelim ve bilgisayar toplandığı taktirde toplam maliyeti de elde edebilelim.
ÇÖZÜM
Adım 1(Component Soyut Sınıfının Tasarlanması)
Herşeyden önce bilgisayardaki tüm parçaları temsil edecek olan Component arayüzünü tasarlayarak başlayalım.
Çoğu bilgisayar parçası tek başına kullanılmaktadır. Dolayısıyla aşağıdaki Component ilkel yani Leaf olarak nitelendirebileceğimiz bilgisayar parçarını temsil edecektir.
//Component
abstract class Part
{
protected string _brand;
protected double _price;
protected Part(string brand, double price)
{
_brand = brand;
_price = price;
}
public virtual double GetPrice()
=> _price;
public string GetBrand()
=> _brand;
}
Kimi parçalarda birden fazla parçanın takılmasıyla meydana gelir. Genel anlamda bu parçadan ziyade bilgisayarın ta kendisidir de diyebiliriz. Haliyle şimdide de Composite nitelikteki parçaları ya da bilgisayarı temsil edecek olan Component‘i tasarlayalım.
//Component
abstract class PartComposite : Part
{
protected List<Part> _parts = new();
protected PartComposite(string brand, double price) : base(brand, price)
{
}
public void AddPart(Part part)
=> _parts.Add(part);
public void RemovePart(Part part)
=> _parts.Add(part);
public override double GetPrice()
{
double totalPrice = _price;
Console.WriteLine($"{_brand} \t\t: {_price}");
foreach (Part part in _parts)
{
Console.WriteLine($"\t{part.GetBrand()} \t: {part.GetPrice()}");
totalPrice += part.GetPrice();
}
return totalPrice;
}
}
Yukarıdaki kod bloğunu incelerseniz eğer ‘Part’ sınıfında virtual olarak ayarlanmış olan ‘GetPrice’ metodu ‘PartComposite’ sınıfında override edilerek içerisinde tüm Composite nesneler için hali hazırda toplam maliyet hesaplaması yapılmaktadır. Bu işlemi tek tek Composite nesnelerde de yapabilirdik lakin tek elden burada yapmak hem daha az maliyetli hem de ekstralara ihtiyaç olmadığı için daha doğru olacaktır.
Adım 2(Composite Sınıfının Tasarlanması)
Ben senaryo gereği tüm parçaların birleştirilmiş hali bilgisayar olarak düşündüğümden dolayı Composite nesne olarak da bilgisayarı temsil edeceğim. Buradaki tercih sizlere kalmış. Farklı parçaları da Composite olarak temsil edebilir ve tasarlayabilirsiniz.
//Composite
class Computer : PartComposite
{
public Computer(string brand, double price) : base(brand, price)
{
}
}
Composite sınıfımızın görüldüğü üzere yukarıdaki gibi ‘PartComposite’den kalıtım alacak şekilde tasarlanması yeterlidir. Nihayetinde alt parçaları barındıran koleksiyon dahil gerekli tüm fonksiyonlar base class’tan geliyor olacaktır.
Adım 3(Leaf Sınıfının Tasarlanması)
Şimdi sıra Leaf sınıflarımızı tasarlamaya gelmiş bulunmaktadır. Sınıflarımızı diyorum çünkü birden fazla bilgisayar parçası mevcuttur. Dolayısıyla tüm parçalara karşılık bir nesne tasarımı gerçekleştireceğiz.(Tabi burada tek bir nesne üzerinden de her bir Leaf‘i instance olarak temsil edebilirdik. Haliyle böyle bir yaklaşımın kararı sizinle ihtiyaçlarınıza kalmıştır)
//Leaf
class Monitor : Part
{
public Monitor(string brand, double price) : base(brand, price)
{
}
}
//Leaf
class Keyboard : Part
{
public Keyboard(string brand, double price) : base(brand, price)
{
}
}
//Leaf
class Ram : Part
{
public Ram(string brand, double price) : base(brand, price)
{
}
}
//Leaf
class Mouse : Part
{
public Mouse(string brand, double price) : base(brand, price)
{
}
}
Adım 4(Test)
Haliyle sıra son olarak yapılan bu tasarımı test etmeye gelmiştir.
Yukarıdaki kod bloğunu derleyip, çalıştırdığımızda aşağıdaki ekran görüntüsüyle karşılaşacağız. İşte bu kadar 🙂
İyi çalışmalar…
SENARYO 5
Bir firma aylık olarak personellerine ödediği maaşların raporlanabileceği bir uygulama istemektedir. Firma bünyesinde aşağıdaki gibi bir hiyerarşi mevcuttur;
Genel müdürlük
Müdürlük
Bölge
Bayi
Personel
Bu hiyerarşide; genel müdürlüğe bağlı müdürlükler, müdürlüklere bağlı bölgeler, bölgelere bağlı bayiler ve son olarak bu bayilerde çalışan personeller olduğu görülmektedir.
Firma maaş hesaplamayı, hem tüm şirket için genel olarak hem de hususi müdürlük, bölge, bayi gibi farklı hiyerarşi seviyeleri için özel olarak istemektedir.
Bir cümle, kelimelerden; kelime ise harflerden meydana gelmektedir. Haliyle böyle bir metinsel düzeni Composite pattern ile gerçekleştirelim.
ÇÖZÜM
Adım 1(Component Soyut Sınıfının Tasarlanması)
Bir metni ifade edecek olan; cümle, kelime ve harf diziliminin ortak yapılanması olan Component arayüzü aşağıdaki gibi olacaktır.
//Component
abstract class Text
{
public abstract void PrintThisBefore();
public abstract void PrintThisAfter();
public virtual void Print()
{
PrintThisBefore();
PrintThisAfter();
}
}
Burada dikkat ederseniz ‘PrintThisBefore’ ve ‘PrintThisAfter’ metotları mevcuttur. Bu metotlar ile ilgili söz öğesinin öncesi ve sonrası durumlarına göre Composite ve Leaf sınıflarında gerekli işaretlemelerde bulunuyor olacağız. Misal olarak; kelimelerden önce boşluğun gelmesi ya da cümlelerin sonunda nokta gelmesi gibi…
//Component
abstract class TextComposite : Text
{
protected List<Text> _texts = new();
public void AddText(Text text)
=> _texts.Add(text);
public void RemoveText(Text text)
=> _texts.Remove(text);
public override void Print()
{
PrintThisBefore();
foreach (Text text in _texts)
text.Print();
PrintThisAfter();
}
}
Adım 2(Composite Sınıfının Tasarlanması)
Bir metin yapılanmasında kelimeler ve cümleler olmak üzere iki adet Composite sınıf bulunmaktadır. Şimdi gelin bu iki sınıfı sırasıyla tasarlayalım;
//Composite
class Word : TextComposite
{
public override void PrintThisBefore()
=> Console.Write(" ");
public override void PrintThisAfter() { /* Boş */ }
}
‘Word’ sınıfına göz atarsanız eğer ‘PrintThisBefore’ metodunda her kelimeden önce gelecek olan boşluk karakteri yazdırılmaktadır. ‘PrintThisAfter’ metodunda ise, her kelimeden sonra boşluk ya da nokta gelebilme ihtimali olduğu için herhangi bir işlem gerçekleştirilmemektedir.
//Composite
class Sentence : TextComposite
{
public override void PrintThisBefore() { /* Boş */ }
public override void PrintThisAfter()
=> Console.Write(".");
}
Benzer mantıkla yukarıdaki ‘Sentence’ sınıfında ise ‘PrinThisBefore’ metodu boş geçilmiş lakin ‘PrinThisAfter’ metodunda ise her cümle sonunda nokta geleceği ifade edilmiştir.
Adım 3(Leaf Sınıfının Tasarlanması)
Metin dizelerinde Leaf olarak tasarlanabilecek tek yapı harflerdir. Haliyle hemen ilgili sınıf tasarımını gerçekleştirelim;
//Leaf
class Letter : Text
{
char _character;
public Letter(char character)
{
_character = character;
}
public override void PrintThisBefore()
=> Console.Write(_character);
public override void PrintThisAfter() { /* Boş */ }
}
‘Letter’ sınıfına göz atarsanız eğer bir ‘char’ türünden karakter almakta ve ‘PrinThisBefore’ metodunda bu karakteri ekrana yazdırmaktadır.
Adım 4(Test)
Yapılan bu inşayı aşağıdaki örnek çalışmayla test edersek eğer;