Chain of Responsibility Design Pattern
Selam dostlar!
Tasarım Desenlerini ele aldığımız makale serisine Chain of Responsibility (CoR) ile devam ediyoruz! Bu tasarım deseni ile temel tasarım desenlerinin üçüncü kategorisi olan Davranışsal Tasarım Desenlerine (behavioral design patterns) giriş yapmış oluyoruz.
Mademki yeni bir kategoriyi ele alıyoruz, o zaman önce bu kategoride yer alan tüm tasarım desenlerinin ortak yönlerini anlamaya çalışalım. İlk olarak, davranışsal kelimesini düşünelim. Bir değer, durum ya da algoritma değişikliği meydana geldiğinde nesnelerinizin bu değişikliğe karşı nasıl bir tepki vereceği, o nesnenin davranışıdır. O halde şöyle özetleyebiliriz: Değişen şeylere karşı geliştirilebilir (esnek) bir altyapı sağlayan tasarım desenleri bu kategori altında yer alıyor.
Hazır genel bir tanım yapmışken, tasarım desenlerinin ortak bir özelliğini daha vurgulamak istiyorum. Bilinen tasarım desenlerinin hepsi; nesneyi kullanan kod bloğunun (ki bu kod bloğuna da istemci diyoruz), temiz ve değişmez olmasını sağlar. İşte tasarım desenlerinin her zaman gündemde olmasının sebebi de aynı zamanda budur.
Bu genel tanımlardan sonra artık daha spesifik olarak Chain of Responsibility tasarım desenine eğilmeye başlayabiliriz. Hazırsanız, her zaman olduğu gibi problem ile başlıyoruz.
Problem
Problemi anlamak için, doğrudan bir senaryo ile başlayalım. Bir Pazartesi günü masanızın başına oturmuş kahvenizi yudumlarken, yeni almış olduğunuz projenin analizine devam ediyorsunuz. Bu proje, tüm dünyada faaliyet gösteren büyük bir sanal ofis firmasının toplantı salonu kiralama uygulamasını geliştirmek. Halihazırda var olan sistem, firmanın çalıştığı ülkelere göre birçok sunucuya ayrılmış durumda. Örneğin Türkiye’den bir toplantı salonu kiralayacaksanız, 150.143.253.114 gibi bir IP adresindeki (URL de olabilir) veritabanını kullanmanız gerekiyor. Sizden istenen, müşteri tarafından belirtilen şehir ve katılımcı sayısına göre gerekli salonların filtrelenerek kullanıcıya sunulması.
Eğer birazdan ele alacağımız tasarım deseni hiç olmasaydı, bu kısmı nasıl kodlardınız? Bir düşünelim. Elimizde çok basit bir koşul var. EĞER istenen yer Türkiye ise, ilgili db’ye bağlan ve gerekli işlemleri yap. O zaman, salonları getirmek için şuna benzer bir kod yazacaktık:
private static void Main(string[] args)
{
var istenenYer = "";
if (istenenYer == "Türkiye")
{
//150.143.253.114 sunucusuna bağlan ve gerekli işlemleri yap
}
else if (istenenYer == "Almanya")
{
//28.158.96.108 sunucusuna bağlan ve gerekli işlemleri yap
}
else if (istenenYer == "Belçika")
{
//107.181.170.186 sunucusuna bağlan ve gerekli işlemleri yap
}
}
İşte problem, bu koda bakınca çok net bir biçimde ortaya çıkıyor. Kodu bir inceleyin. Böyle apartman gibi if (ya da switch case) bloğunun istemci kodu için çok verimsiz ve bakım maliyetini arttıran bir yapı olduğu su götürmez bir gerçek. Bu kirli bir kod. Çünkü, ihtiyaçlarınız değiştiğinde bu kodu da değiştirmek zorunda kalacaksınız. Uygulamayı geliştirdiğimiz müşteri, yeni bir ülkeyle çalışmaya başladığında yukarıdaki yapıya bir if daha eklemek geliştiriciyi de müşteriyi de çıldırtacak bir durumdur.
Çözüm
Peki ne yapmalı? Nasıl bir mimari kullanmalı? Gelin bunun üzerine biraz kafa patlatalım. Şimdi biz istemcinin if bloğu ile bu ihtiyacı çözmesini istemiyoruz. Onun yerine bir nesne oluşturmak ve bu nesnenin ilgili metodunu çağırmak çok daha iyi olur. Tamam. Şimdi bu metoda göre soruları biraz daha detaylandırarak ilerleyelim.
Bu metot ne iş yapmalı? Kullanıcının kiralamak istediği salonun yeri, katılımcı bilgisi gibi verilere göre ilgili sunucudaki veritabanına bağlanıp filtrelenmiş sonucu döndürmeli. Bu cümleden yazacağımız metodun, arama kriterlerini parametre olarak alacağı anlaşılıyor. Tabii ki bu arama kriterlerini bir nesne içinde tutacağız. Buraya kadar metodun gövdesi oluşmuş durumda. Peki, koşul yapısını nasıl sağlayacağız?
Unutmayın! Amacımız if bloğunun verimsizliğinden kurtulmaktı. Peki, if bloğunda kullandığımız her koşuldan farklı bir nesnenin sorumlu olmasını sağlarsak ve bu nesnelerinin her birini de birbiriyle ilişkilendirirsek nasıl olur?
Gerçek Dünyadan Bir Örnek
Karışık mı geldi? O zaman gerçek hayatımıza bir bakalım. Evde internetiniz kesildi (az önce gerçekten başıma geldi bu arada) ve genel kontrolleri yaptıktan sonra çözemeyip servis sağlayıcınızın müşteri numarasını tuşladınız. Karşınıza ilk olarak bir ses kaydı geldi ve genel kategorileri sayarak sizi tuşlara yönlendirmeye başladı… “Fatura işlemleri için 1’e” vesaire diye sayarken, ilgili kategoriye geldiniz ve operatöre bağlanmak için beklemeye başladınız. Bir süre daha geçti (bu arada tabii ki bir reklam cıngılı dinliyorsunuz 😊) ve operatörle konuşmaya başladınız.
Operatör size bazı sorular yöneltti. Verdiğiniz yanıtlara göre, eğer yaşadığınız sorun operatörün sorumluluğu altındaysa, size yardımcı olacaktır. Fakat değilse? O zaman sizi yetkisi olduğunu düşündüğü bir diğer kişiye bağlayacaktır. Bu prosedür, sorun çözülene dek devam edecek bir yapıdır.
İşte bu örnek, tam olarak Chain Of Responsibility (CoR – Sorumluluk Zinciri) tasarım deseninin karşılığı. Şimdi gelin buradaki nesneleri birer birer ayıralım ve ilişkileri ortaya dökelim. Karşımıza çıkan ilk nesne, bizi ilk karşılayan operatör. Bu operatör bize bazı sorular sorarak bir takım veriler elde etmişti. İşte bu veriler de bir diğer nesne. Operatör nesnesi, müşteri verisi nesnesini değerlendirdi ve sorumluluğun kendisinde olmadığını gördü. Bu durumda müşteri verisini kendisine bağlı diğer operatöre aktardı. Bu işlem, zincirin son sırasındaki operatöre dek devam edecek.
Kodlama Zamanı
Şimdi senaryomuza dönüp Chain of Responsibility tasarım desenini uygulamaya başlayabiliriz. Yukarıda çözüm kısmında, kullanıcının belirttiği arama kriterlerinin nesne olması gerektiğini tespit etmiştik zaten. O zaman ilk adımımız bu nesnenin sınıfını oluşturmak olsun.
public class AramaKriteri
{
public string Ulke { get; set; }
public string Sehir { get; set; }
public int KatilimciSayisi { get; set; }
public DateTime TalepTarihi { get; set; }
}
İstemcinin erişeceği ilk nesne (gerçek dünya örneğindeki ilk operatör), zincirin ilk halkası. Bu nesnenin Ara metodu da bizden AramaKriteri nesnesini alacak ve sorumlu nesneye (zincirin sonraki halkasına) doğru aktaracak. Bu durumda, zincirin tüm halkaları bir üst sınıftan türemeli. Hatta her nesne uygun salon arama işini ayrı bir sunucu ile çalışacağına göre bu üst sınıf ve ara metodu abstract olmalı! Bu sınıfa, ToplantiSalonRezervasyon diyelim. Zincirin her halkası, bir sonraki halkaya erişebileceğine göre, kendi tipinde bir özellik taşıması gerekiyor. Bu özelliğe de BirSonrakiSorumlu diyelim. Şimdi, nesnenin kendisinden bir sonraki nesneye veriyi aktarmasının en kolay yolu delege kullanmaktır. Çünkü, veri aktarılır aktarılmaz, söz konusu metodun anında tetiklenmesini istiyoruz. Bu amaçla EventHandler generic delegesinden faydalanacağız. E tabii ki bu delegenin fırlatacağı metodu da oluşturmamız gerekiyor. Son olarak, delege ve metodu, ToplantiSalonRezervasyon sınıfının contstuctor’unda eşleştireceğiz. Toparlarsak:
public abstract class ToplantiSalonRezervasyon
{
//Zincirin bir üst halkası
public ToplantiSalonRezervasyon BirSonrakiSorumlu { get; set; }
//kriteri yakalayıcı
private EventHandler<AramaKriteri> aramaKriteriHandler;
//kriter yakalandığında çalışacak metot
protected abstract void ara(object sender, AramaKriteri kriter);
public ToplantiSalonRezervasyon()
{
//ara metodunu delege'ye aktar:
aramaKriteriHandler += ara;
}
}
ToplantiSalonRezervasyon sınıfına istemciden erişecek olan metodumuzu henüz eklemedik. Bu metot, yukarıda çözüm kısmında planladığımız ilk metot olacak. O zaman yukarıdaki yapıya göre sadece, ekleyeceğimiz metot yalnızca delegemizi çağıracak o kadar. Aşağıdaki metodu da ToplantiSalonRezervasyon sınıfına ekliyorum.
public void UygunSalonlariAra(AramaKriteri kriter)
{
aramaKriteriHandler(this, kriter);
}
Artık zincirimizin tüm halkalarını oluşturabiliriz. Burada önemli olan, her halkanın kendi sorumluluğunu bilmesi. Eğer işlemi yapmaktan sorumlu değilse, ilgili veriyi bir sonraki halkaya fırlatması. Aslında bu halkaların arasında bir hiyerarşi de oluşturuluyor. Bu hiyerarşinin nasıl oluşacağını elbette sizin senaryonuz belirleyecektir. Gelsin Halkalar:
//1. Halka
public class AlmanyaRezervasyon : ToplantiSalonRezervasyon
{
protected override void ara(object sender, AramaKriteri kriter)
{
if (kriter.Ulke == "Almanya")
{
Console.WriteLine("Almanya için uygun salonlar aranıyor");
}
else
{
//birsonrakiSorumlu boş değilse
BirSonrakiSorumlu?.UygunSalonlariAra(kriter);
}
}
}
//2. Halka
public class BelcikaRezervasyon : ToplantiSalonRezervasyon
{
protected override void ara(object sender, AramaKriteri kriter)
{
if (kriter.Ulke == "Belçika")
{
Console.WriteLine("Belçika için uygun salonlar aranıyor");
}
else
{
BirSonrakiSorumlu?.UygunSalonlariAra(kriter);
}
}
}
//3. Halka
public class TurkiyeRezervasyon : ToplantiSalonRezervasyon
{
protected override void ara(object sender, AramaKriteri kriter)
{
if (kriter.Ulke == "Türkiye")
{
Console.WriteLine("Türkiye için uygun salonlar aranıyor");
}
}
}
Şimdi, artık istemci koduna geçerek temiz kodumuzu gözlemleme zamanı. İlk olarak, üç halka nesnesini oluşturacağım ve belirlediğim sıraya göre, birleştireceğim. Ardından da zincirin ilk halkasına talepte bulunacağım. Bakalım sonuç ne olacak?
private static void Main(string[] args)
{
AlmanyaRezervasyon almanyaRezervasyon = new AlmanyaRezervasyon();
BelcikaRezervasyon belcikaRezervasyon = new BelcikaRezervasyon();
TurkiyeRezervasyon turkiyeRezervasyon = new TurkiyeRezervasyon();
almanyaRezervasyon.BirSonrakiSorumlu = belcikaRezervasyon;
belcikaRezervasyon.BirSonrakiSorumlu = turkiyeRezervasyon;
almanyaRezervasyon.UygunSalonlariAra(new AramaKriteri { KatilimciSayisi = 15, Ulke = "Türkiye" });
Console.ReadLine();
}
Evet. Gördüğünüz gibi önce üç halka oluşturdum ve sonra bu halkaların birleşiminden oluşan zincirimi inşa ettim.
Ardından da bu zincirin ilk halkasına ihtiyacım olan talebi gönderdim. Tabii ki çıktı geliyor:
Evet sevgili dostlar, artık elimizde koşul da değişse, yeni bir sunucu da gelse geliştirilebilir bir alt yapı var. Koşul değişirse, ilgili nesnenin metodu, değişecek, yeni sunucu gelirse zincire yeni bir halka eklenecek. Hepsi bu.
Böylece CoR tasarım desenini burada tamamlamış olduk. Umarım faydası olmuştur.
Görüşmek üzere.
yine faydalı bir makale olmuş ustad emeğine sağlık.
beklentimiz zincirin her halkasının kendi sorumluluğundaki işleri gerçekleştirmesi değil mi?
örneğimizde sanki ilgili sorumlu zincir halkası aranıyor, bulunduğunda da işi sadece o yapıyor
Selamlar. Aslında, zincirin her halkası kendi sorumluluğunu gerçekleştiriyor. Eğer sorumlu değilse bir üst nesneye paslıyor. Şu koddaki ara metoduna dikkat:
public class AlmanyaRezervasyon : ToplantiSalonRezervasyon
{
protected override void ara(object sender, AramaKriteri kriter)
{
if (kriter.Ulke == “Almanya”)
{
Console.WriteLine(“Almanya için uygun salonlar aranıyor”);
}
else
{
//birsonrakiSorumlu boş değilse
BirSonrakiSorumlu?.UygunSalonlariAra(kriter);
}
}
}
Eğer ülke Almanya ise işini yap, değilse bir üst halkaya git şeklinde. Dolayısı ile sorumluyu aramıyor, sadece sorumlu değilse bir üste fırlatıyor.
Düğün salonlari vb. işler içinde rezervasyonlarımız olsun ve her ülkede olmaması halinde nasıl ilerleteceğiz kodumuzu ? Bu örnek sanki OCP uymuyor gibi gözüküyor yada ben mi uyduramıyorum 🙂
Nasıl çözebiliriz bu sorunu ?
Eğer otel rezervasyonları yerine, düğün salonları da işin içine girerse, bu tamamen farklı bir sınıfın görevi olur. Dolayısıyla yukarıdaki örnekten bağımsız bir mimari ile çözmek gerekir.