Chain of Responsibility Pattern - Sorumluluk Zinciri Tasarım Deseni
11 min read

Chain of Responsibility Pattern - Sorumluluk Zinciri Tasarım Deseni

Chain of Responsibility Pattern - Sorumluluk Zinciri Tasarım Deseni
Photo by Stephen Leonardi / Unsplash

Chain of Responsibility, istekleri bir işleyici zinciri boyunca iletmenize izin veren behavioral bir tasarım modelidir. Bir istek aldıktan sonra, her işleyici isteği işleme koymaya veya zincirdeki bir sonraki işleyiciye iletmeye karar verir.

Daha önceki Tasarım Deseni / Kalıbı / Örüntüsü (Design Pattern) Nedir? başlıklı yazımızda tanıttığımız Behavioral Patterns altında yer alan Chain of Responsibility Nedir sorusunu ve kod örneklerini bu yazımızda oldukça detaylı paylaşıyoruz.

Chain of Responsibility

Bir çevrimiçi sipariş sistemi üzerinde çalıştığınızı hayal edin. Yalnızca kimliği doğrulanmış kullanıcıların sipariş oluşturabilmesi için sisteme erişimi kısıtlamak istiyorsunuz. Ayrıca, yönetici izinlerine sahip kullanıcıların tüm siparişlere tam erişimi olmalıdır.

Biraz planladıktan sonra bu kontrollerin sırayla yapılması gerektiğini anladınız. Uygulama, kullanıcının kimlik bilgilerini içeren bir istek aldığında sistemde bir kullanıcının kimliğini doğrulamayı deneyebilir. Ancak, bu kimlik bilgileri doğru değilse ve kimlik doğrulama başarısız olursa, başka kontrollere devam etmek için hiçbir neden yoktur.

İstek, sipariş sisteminin kendisinin işleyebilmesi için bir dizi kontrolden geçmelidir.

Önümüzdeki birkaç ay boyunca, bu sıralı kontrollerden birkaçını daha uyguladınız.

  • Ekip arkadaşlarınızdan biri, ham verileri doğrudan sipariş sistemine aktarmanın güvenli olmadığını öne sürdü. Böylece, bir istekteki verileri sterilize etmek için fazladan bir doğrulama adımı eklediniz.
  • Daha sonra birisi, sistemin brute force şifre kırmaya karşı savunmasız olduğunu fark etti. Bunu engellemek için hemen aynı IP adresinden gelen tekrarlanan başarısız istekleri filtreleyen bir kontrol eklediniz.
  • Başka biri, aynı verileri içeren tekrarlanan isteklerde önbelleğe alınmış sonuçları döndürerek sistemi hızlandırabileceğinizi önerdi. Bu nedenle, yalnızca önbelleğe alınmış uygun bir yanıt olmadığında isteğin sisteme geçmesine izin veren başka bir kontrol eklediniz.
Kod büyüdükçe, daha karmaşık hale geldi.

Zaten karmakarışık görünen kontrollerin kodu, her yeni özelliği ekledikçe daha da şişirildi. Ayrıca bir kontrolleri değiştirmek bazen diğerlerini de etkiler. Hepsinden kötüsü, sistemin diğer bileşenlerini korumak için kontrolleri yeniden kullanmaya çalıştığınızda, bu bileşenler bazı kontrolleri gerektirir, kodun bir kısmını çoğaltmanız ve duplike hale getirmeniz gerekiyor.

Sistemin anlaşılması çok zor ve bakımı pahalı hale geldi. Bir süre kodla uğraştınız, ta ki bir gün her şeyi yeniden düzenlemeye karar verene kadar.

Çözüm

Diğer birçok behavioral tasarım desenigibi, Chain of Responsibility de belirli davranışları handler adı verilen bağımsız nesnelere dönüştürmeye dayanır. Bizim durumumuzda, her kontrol, kontrolü gerçekleştiren tek bir yöntemle kendi sınıfına extract edilmelidir. İstek, verileriyle birlikte bu yönteme bir argüman olarak iletilir.

Desen, bu handler'ları bir zincire bağlamanızı önerir. Her bağlantılı handler, zincirdeki bir sonraki handler'a bir referans depolamak için bir alana sahiptir. Bir isteği işlemeye ek olarak, handler'lar isteği zincir boyunca iletir. İstek, tüm handler'lar onu işleme şansı bulana kadar zincir boyunca ilerler.

İşin en iyi yanı şudur: Bir handler, talebi zincirin daha aşağılarına iletmemeye ve daha fazla işlemeyi etkili bir şekilde durdurmaya karar verebilir.

Sipariş sistemleriyle ilgili örneğimizde, bir handler işlemeyi gerçekleştirir ve ardından talebi zincirin daha ilerisine geçirip geçirmemeye karar verir. İsteğin doğru verileri içerdiğini varsayarsak, tüm handler'lar, kimlik doğrulama kontrolleri veya önbelleğe alma olsun, birincil davranışlarını yürütebilir.

Handler'lar tek tek sıralanarak bir zincir oluşturur.

Ancak, biraz farklı bir yaklaşım vardır (ve biraz daha kurallıdır), bir istek alındığında handler bunu işleyip işleyemeyeceğine karar verir. Yapabiliyorsa, isteği daha fazla iletmez. Yani ya isteği işleyen tek bir handlerdır ya da hiçtir. Bu yaklaşım, bir GUI içindeki öğe yığınlarındaki olaylarla ilgilenirken çok yaygındır.

Örneğin, bir kullanıcı bir düğmeyi tıklattığında olay, düğme ile başlayan, kapsayıcıları (formlar veya paneller gibi) boyunca ilerleyen ve ana uygulama penceresi ile biten GUI öğeleri zinciri boyunca yayılır. Olay, zincirdeki onu işleyebilen ilk eleman tarafından işlenir. Bu örnek ayrıca bir nesne ağacından bir zincirin her zaman çıkarılabileceğini gösterdiği için dikkate değerdir.

Bir nesne ağacının dalından bir zincir oluşturulabilir.

Tüm handler sınıflarının aynı interface'i implemente etmesi çok önemlidir. Her concrete handler, yalnızca execute yöntemine sahip olanla ilgilenmelidir.

🚗 Gerçek Dünya Analoji

Bilgisayarınıza yeni bir donanım parçası satın aldınız ve kurdunuz. Fakat nerd birisi olduğunuz için bilgisayarda birkaç işletim sistemi kuruludur :) Donanımın desteklenip desteklenmediğini görmek için hepsini boot etmeye çalışırsınız. Windows, donanımı otomatik olarak algılar ve etkinleştirir. Ancak sevgili Linux'unuz yeni donanımla çalışmayı reddediyor. Küçük bir umut ışığıyla kutunun üzerinde yazılı olan teknik destek telefon numarasını aramaya karar verdiniz.

Teknik desteğe yapılan bir çağrı, birden çok operatör aracılığıyla yapılabilir.

İlk duyduğunuz şey, otomatik yanıtlayıcının robotik sesidir. Hiçbiri sizin durumunuzla ilgili olmayan çeşitli sorunlara dokuz popüler çözüm önerir. Bir süre sonra robot sizi canlı bir operatöre bağlar.

Ne yazık ki, operatör de belirli bir şey öneremez. Yorumlarınızı dinlemeyi reddederek, kılavuzdan uzun alıntılar yapıp dururlar. “Bilgisayarı kapatıp tekrar açmayı denediniz mi?” ifadesini duyduktan sonra. 10. kez uygun bir mühendise bağlanmayı talep ediyorsunuz.

Sonunda operatör, aramanızı bir ofis binasının karanlık bodrum katındaki yalnız sunucu odasında otururken muhtemelen saatlerce canlı bir insan sohbeti özlemi çeken mühendislerden birine iletir. Mühendis, yeni donanımınız için uygun sürücüleri nereden indireceğinizi ve bunları Linux'a nasıl kuracağınızı söyler. Sonunda, çözüm! Çağrıyı sevinçten uçarak bitiriyorsunuz.

⚓ Structure

Structure
  1. Handler, tüm concrete handlerlar için ortak olan interface'i bildirir. Genellikle istekleri işlemek için yalnızca tek bir yöntem içerir, ancak bazen zincirdeki bir sonraki işleyiciyi ayarlamak için başka bir yöntemi de olabilir.
  2. Base Handler, tüm handler sınıflarında ortak olan boilerplate kodunu koyabileceğiniz isteğe bağlı bir sınıftır. Genellikle bu sınıf, bir sonraki işleyiciye bir başvuruyu depolamak için bir field tanımlar. İstemciler, bir işleyiciyi önceki işleyicinin constructor'ına veya setter'ına ileterek bir zincir oluşturabilir. Sınıf ayrıca varsayılan işleme davranışını da uygulayabilir: varlığını kontrol ettikten sonra yürütmeyi bir sonraki handler'a aktarabilir.
  3. Concrete Handler'lar, istekleri işlemek için gerçek kodu içerir. Bir istek alındığında, her handler, onu işleyip işlemeyeceğine ve ayrıca zincir boyunca iletip iletemeyeceğine karar vermelidir. Handler'lar genellikle bağımsızdır ve değişmezdir, constructor aracılığıyla gerekli tüm verileri yalnızca bir kez kabul eder.

# Chain Of Responsibility Pseudocode

Bu örnekte, Chain Of Responsibility deseni, etkin GUI öğeleri için bağlamsal yardım bilgilerini görüntülemekten sorumludur.

GUI sınıfları, Composite desenle oluşturulmuştur. Her öğe, kapsayıcı öğesiyle bağlantılıdır. Herhangi bir noktada, öğenin kendisiyle başlayan ve tüm kapsayıcı öğelerinden geçen bir öğeler zinciri oluşturabilirsiniz.

Uygulamanın GUI'si genellikle bir nesne ağacı olarak yapılandırılmıştır. Örneğin, uygulamanın ana penceresini oluşturan Dialog sınıfı, nesne ağacının kökü olacaktır. Dialog kutusu, diğer Panelleri veya Button'ları ve TextField'lar gibi basit düşük seviyeli öğeleri içerebilen Panel'leri içerir.

Basit bir bileşen, bileşene atanmış bazı yardım metinlerine sahip olduğu sürece kısa bağlamsal araç ipuçlarını gösterebilir. Ancak daha karmaşık bileşenler, kılavuzdan bir alıntı göstermek veya bir tarayıcıda bir sayfa açmak gibi bağlamsal yardım göstermenin kendi yollarını tanımlar.

Bir yardım isteği, GUI nesnelerini bu şekilde geçer.

Bir kullanıcı fare imlecini bir öğeye yönelttiğinde ve F1 tuşuna bastığında, uygulama işaretçinin altındaki bileşeni algılar ve ona bir yardım isteği gönderir. İstek, yardım bilgilerini görüntüleyebilen öğeye ulaşana kadar tüm öğe kapsayıcılarında baloncuk oluşturur.

// Handler interface'i, bir handler zinciri oluşturmak için bir
// yöntem bildirir. Ayrıca, bir isteği yürütmek için bir yöntem bildirir.
interface ComponentWithContextualHelp is
    method showHelp()


// Basit componentler için base class
abstract class Component implements ComponentWithContextualHelp is
    field tooltipText: string

    // Componentin kapsayıcısı, 
    // handler'lar zincirinde bir sonraki bağlantı görevi görür.
    protected field container: Container

    // Component, kendisine atanmış yardım metni varsa bir araç ipucu gösterir. 
    // Aksi takdirde, çağrıyı varsa konteynere iletir.
    method showHelp() is
        if (tooltipText != null)
            // tooltipi gösterir.
        else
            container.showHelp()


// Containerlar hem basit bileşenleri hem de diğer kapsayıcıları alt öğe olarak içerebilir. 
// Zincirleme ilişkiler burada kurulur. Sınıf, showHelp davranışını ebeveyninden devralır.
abstract class Container extends Component is
    protected field children: array of Component

    method add(child) is
        children.add(child)
        child.container = this


// İlkel bileşenler varsayılan yardımla iyi olabilir
// implementation...
class Button extends Component is
    // ...

// Ancak karmaşık bileşenler, varsayılan uygulamayı geçersiz kılabilir. 
// Yardım metni yeni bir şekilde sağlanamazsa, bileşen her zaman temel uygulamayı çağırabilir.
// (bkz. Component sınıfı).
class Panel extends Container is
    field modalHelpText: string

    method showHelp() is
        if (modalHelpText != null)
            // Yardım metniyle kalıcı bir pencere gösterir
        else
            super.showHelp()

// ...Yukarıdaki ile aynı...
class Dialog extends Container is
    field wikiPageURL: string

    method showHelp() is
        if (wikiPageURL != null)
            // Wiki yardım sayfasını açar
        else
            super.showHelp()


// Client code.
class Application is
    // Her uygulama zinciri farklı şekilde yapılandırır.
    method createUI() is
        dialog = new Dialog("Budget Reports")
        dialog.wikiPageURL = "http://..."
        panel = new Panel(0, 0, 400, 800)
        panel.modalHelpText = "This panel does..."
        ok = new Button(250, 760, 50, 20, "OK")
        ok.tooltipText = "This is an OK button that..."
        cancel = new Button(320, 760, 50, 20, "Cancel")
        // ...
        panel.add(ok)
        panel.add(cancel)
        dialog.add(panel)

    // Burada ne olduğunu hayal et.
    method onF1KeyPress() is
        component = this.getComponentAtMouseCoords()
        component.showHelp()

💡 Uygulanabilirlik

🐛 Programınızın farklı türde istekleri çeşitli şekillerde işlemesi beklendiğinde, ancak isteklerin tam türleri ve sıraları önceden bilinmediğinde Sorumluluk Zinciri modelini kullanın.

⚡ Desen, birkaç handler'ı tek bir zincire bağlamanıza ve bir istek aldığınızda, her handler'a onu işleyip işleyemeyeceğine "sormanıza" olanak tanır. Bu şekilde tüm handler'ların isteği işleme alma şansı olur.

🐛 Belirli bir sırayla birkaç handler yürütmek gerekli olduğunda kalıbı kullanın.

⚡ Zincirdeki handlerları herhangi bir sırayla bağlayabildiğiniz için, tüm istekler tam olarak planladığınız gibi zincirden geçecektir.

🐛 Handler'lar kümesinin ve sıralarının çalışma zamanında değişmesi gerektiğinde CoR modelini kullanın.

⚡ Handler sınıfları içinde bir referans alanı için ayarlayıcılar sağlarsanız, handlerları dinamik olarak ekleyebilir, kaldırabilir veya yeniden sıralayabilirsiniz.

🗎 Nasıl Implemente Edeceğiz?

  1. Handler interface'ini bildirin ve istekleri işlemek için bir yöntemin imzasını açıklayın. İstemcinin istek verilerini yönteme nasıl ileteceğine karar verin. En esnek yol, isteği bir nesneye dönüştürmek ve onu bir argüman olarak handler yöntemine iletmektir.
  2. Concrete handlerlarda yinelenen boilerplate kodunu ortadan kaldırmak için, handler interface'inden türetilen soyut bir base handler sınıfı oluşturmaya değer olabilir. Bu sınıf, zincirdeki bir sonraki işleyiciye bir referans depolamak için bir field'a sahip olmalıdır. Sınıfı immutable yapmayı düşünün. Ancak zincirleri çalışma zamanında değiştirmeyi planlıyorsanız, referans alanının değerini değiştirmek için bir ayarlayıcı tanımlamanız gerekir. Handler yöntemi için uygun varsayılan davranışı da uygulayabilirsiniz; bu hiç kalmadığı sürece isteği bir sonraki nesneye iletecek yöntem olabilir. Concrete handler'lar, ebeveyn yöntemini çağırarak bu davranışı kullanabilecektir.
  3. Tek tek concrete handler alt sınıfları oluşturun ve bunların handler yöntemlerini implemente edin. Her işleyici, bir istek alırken iki karar vermelidir: İsteği işleyip işlemeyeceği ve isteği zincir boyunca geçirip geçirmeyeceği.
  4. Müşteri, zincirleri kendi başına monte edebilir veya diğer nesnelerden önceden oluşturulmuş zincirler alabilir. İkinci durumda, yapılandırma veya ortam ayarlarına göre zincirler oluşturmak için bazı factory sınıflarını uygulamanız gerekir.
  5. Zincirin dinamik doğası gereği, müşteri aşağıdaki senaryoları işlemeye hazır olmalıdır:
  • Zincir tek bir halkadan oluşabilir.
  • Bazı istekler zincirin sonuna ulaşmayabilir.
  • Diğerleri zincirin sonuna işlenmeden ulaşabilir.

⚖️ Chain of Responsibility Avantajları ve Dezavantajları

✔️ İstek handling sırasını kontrol edebilirsiniz.

✔️ Single Responsibility Principle. Handler'ları çağıran sınıfları, handler'ları gerçekleştiren sınıflardan ayırabilirsiniz.

✔️ Open/Closed Principle. Mevcut istemci kodunu bozmadan uygulamaya yeni handler'lar tanıtabilirsiniz.

❌ Bazı istekler handling edilmeden sona erebilir.

⇄ Diğer Desenlerle İlişkisi

  • Chain of Responsibility, Command, Mediator ve Observer talep gönderenleri ve alıcıları birbirine bağlamanın çeşitli yollarını ele alır:
  1. Chain of Responsibility, bir talebi işleyene kadar potansiyel alıcılardan oluşan dinamik bir zincir boyunca sırayla iletir.
  2. Command, göndericiler ve alıcılar arasında tek yönlü bağlantılar kurar.
  3. Mediator, göndericiler ve alıcılar arasındaki doğrudan bağlantıları ortadan kaldırarak onları bir aracı nesne aracılığıyla dolaylı olarak iletişim kurmaya zorlar.
  4. Observer, alıcıların isteklere dinamik olarak abone olmalarını ve abonelikten çıkmalarını sağlar.
  • Chain of Responsibility genellikle Composite pattern ile birlikte kullanılır. Bu durumda, bir leaf component bir istek aldığında, onu tüm ana bileşenlerin zincirinden geçerek nesne ağacının köküne kadar iletebilir.
  • Chain of Responsibility deki handlerlar, Command olarak implemente edilebilir. Bu durumda, bir istek tarafından temsil edilen aynı bağlam nesnesi üzerinde birçok farklı işlemi gerçekleştirebilirsiniz. Ancak, isteğin kendisinin bir Command nesnesi olduğu başka bir yaklaşım daha vardır. Bu durumda, aynı işlemi bir zincire bağlı bir dizi farklı bağlamda yürütebilirsiniz.
  • Chain of Responsibility ve Decorator çok benzer sınıf yapılarına sahiptir. Her iki desen de yürütmeyi bir dizi nesneden geçirmek için özyinelemeli bileşime dayanır. Ancak, birkaç önemli farklılık vardır. CoR handlerları, birbirinden bağımsız olarak isteğe bağlı işlemleri yürütebilir. Ayrıca, herhangi bir noktada isteği daha fazla iletmeyi bırakabilirler. Öte yandan, çeşitli Decoratorlar, nesnenin davranışını temel arayüzle tutarlı tutarken genişletebilir. Ayrıca, Decoratorların talebin akışını kesmesine izin verilmez.

<> Kod Örneği

Aşağıdaki public repo adresine giderek adresine giderek örnek Singleton kodlarını inceleyebilirsiniz.

Github repoya gitmek için Tıklayınız.