Bu yazı Design Patterns/Tasarım Desenleri nedir? başlıklı yazı dizisinin bir parçasıdır.

Bu içerik ağırlıklı olarak refactoring.guru sitesindeki içeriğin tercümesi ve derlenmesinden oluşturulmuştur.

Tüm tasarım desenleri ya da diğer adıyla tasarım kalıplarına yönelik ayrıntılı içeriklere yazının sonundaki bağlantılardan ulaşabilirsiniz.

Chain of Responsibility deseninin amacı

Chain of Responsibility, Türkçe adıyla “Sorumluluk Zinciri” bir isteği bir dizi işleyici (zinciri) boyunca iletmenize izin veren davranışsal bir tasarım desenidir (behavirolar design pattern) ve inceleyeceğimiz davranışsal tasarım desenlerinden de ilkidir. Eğer bir web geliştirici iseniz “Middleware” tasarımlarında oldukça işinize yarayacak bir tasarım desenidir.

Sorun

Çevirim içi sipariş alımı yapılacak bir sistem üzerinde çalıştığınızı hayal edin. Sisteme girişlerin sınırlı olmasını, sadece onaylanmış kullanıcıların sipariş vermesini istiyorsunuz. Ayrıca yönetici yetkisindeki kullanıcıların da tüm siparişlere tam erişim yetkisi olmalı.

Bir süre düşündükten sonra bu kontrollerin sırayla yapılması gerektiğini farkettiniz. Uygulama kullanıcı giriş bilgileri içeren bir istek aldığında kullanıcıyı doğrulamayı deneyebilir. Eğer giriş bilgileri hatalıysa diğer kontrolleri yapmanıza gerek olmayacaktır.

İstek sipariş sistemi işleme almadan önce bir dizi kontrolden geçmelidir.

İlerleyen aylarda başka sıralı kontrollerde eklemeniz gerekti;

  • İş arkadaşlarınızdan biri sipariş sistemine kontrolden geçmemiş ham verinin gönderilmesinin güvensiz olduğunu söyledi ve sizde istek içerisindeki veriyi temizlemek için ekstra bir adım eklediniz
  • Daha sonra bir başkası sistemin rastgele şifre kırma denemelerine (brute force) karşı korumalı olmadığını söyledi ve sizde aynı IP adresinden başarısız giriş denemelerini filtreleyen bir adım daha eklediniz.
  • Daha sonra birisi sonuçları ön belleğe alarak sistemi hızlandırabileceğinizi söyledi ve bunun üzerine ön bellekte uygun sonuç varsa istekleri sipariş sistemine hiç göndermemeye başladınız.
İstek sipariş sistemi işleme almadan önce bir dizi kontrolden geçmelidir.

Bu kontrolleri yaptığınız ve zaten yeterince şişmiş kod yığını her yeni özellik eklediğinizde daha da karmaşık hale geliyor. Bir kontrolü değiştirmek bazen diğerlerine de etki ediyor. En kötüsü bu kontrolleri bazılarını sistemin başka yerlerinde de kullanmak istediğinizde kodun belirli bölümlerini kopyala yapıştır yapmanız gerekiyor, çünkü sadece bazı kontroller gerektiği için olduğu gibi kullanamıyorsunuz.

Sistem en sonunda takip edilmesi ve bakımı çok zor bir sistem haline geliyor. Bir süre kodla boğuşmaya devam etseniz de en sonunda kodu aşağıdaki çözüme göre yeniden düzenlemeye karar veriyorsunuz.

Çözüm

Sorumluluk zinciri (chain of responsibility), diğer bir çok davranışsal tasarım modelinde olduğu gibi, belirli davranışları işleyici adını verdiği bazı nesnelere dönüştürerek onlara yaptırmaya dayanır. Yukarıdaki örnekte her adım, tek bir kontrol metodu içeren bağımsız bir işleyici sınıfa (handler class) dönüştürülür. İstek tüm verisi ile beraber bu metoda bir parametre olarak gönderilir.

Sorumluluk zinciri deseni bu işleyici sınıfları bir zincir şeklinde birbirine bağlamanızı önerir. Zincirdeki her işleyicide bir sonraki işleyicinin referansını tuttuğu bir alan vardır. İstek bu zincirin başından sonuna kadar tüm işleyicilerden geçer.

İşin güzel tarafı işleyiciler isteği zincirin bir sonraki adımına göndermemeyi tercih edebilir ve böylece işlemi durdurulabilir. Daha ilk aşamada durması gereken bir istek sonraki aşamalara hiç aktarılmaz. Kullanıcı girişi hatalıysa, istek parametrelerini filtrelemeniz, sonuçlar ön bellekte var m bakmanız, yoksa veri tabanından sorgulamanız vs. vs. gerekmez. Ya da ön bellek adımına kadar gelen bir istek, ön bellek işleyicisi sonucu ön bellekte bulduysa sonraki adımlara aktarılmaz ve ön bellek işleyicisi tarafından sonuçlar gönderilir.

İşleyiciler bir zincir oluşturacak şekilde sıralanmışlar.

Çok benzer ama ufak farklılıkları olan bir yaklaşım daha var. Bu yaklaşımda bir işleyici isteği aldıktan sonra işleyip işleyemeyeceğine karar veriyor ve işleyebileceği bir istekse bunu sonraki adımlara göndermiyor. Yani sonuç olarak istek en fazla bir işleyici tarafından işleniyor ( tabii ki zincirin sonuna kadar gidip hiç biri tarafından işlenemeyebilir ) Bu yaklaşım özellikle grafik arabirimi içindeki öğe yığınlarının (stack) olayları ( event ) ile ilgilenirken çok yaygın kullanılır.

Örneğin bir butona tıklandığında, tıklama olayı öncellikle o buton, sonra o butonun bulunduğu kapsayıcı, onun bir üst kapsayıcısı, doküman vs. şeklinde ağacın en üstüne kadar takip edilir ve uygulamanın davranışına göre tıklama olayı ilk olay tanımlanan yerden yukarıya aktarılabilir ya da aktarılmayabilir.

Bir nesne ağacının dallarından bir zincir tanımlanabilir.

Tüm işleyici sınıflarının ( handler class ) aynı ara yüzü ( interface ) esas almaları çok önemlidir. Her işleyici, bir sonraki işeyicinin aynı çalıştırılabilir metoda sahip olduğunu bilmelidir. Böylece işleyicilerin sınıflarına bağımlı kalmadan çalışma esnasında istediğiniz zinciri oluşturabilirsiniz.

Uygulanabilirlik

Programınızın farklı türdeki istekleri çeşitli şekillerde işlemesi gerekiyor, fakat bu isteklerin türleri ve sıralamalarını önceden bilmiyorsanız Chain of Responsibility desenini kullanabilirsiniz.

Bu model bir kaç işleyiciden oluşan bir zincir oluşturmanıza ve bir istek geldiğinde bu zincirdeki işleyicilerin hangisi veya hangilerinin bu isteği işleyeceklerine karar vermelerine olanak tanır. Bu şekilde tüm işleyiciler isteği işleme alma şansına sahip olur.

Belirli bir sırayla bir kaç işleyiciyi yürütmek gerekli olduğunda chain of responsibility desenini kullanabilirsiniz.

Zincirdeki işleyicileri herhangi bir sırayla bağlayabildiğiniz için istekler zincirden tam olarak planladığınız sırada geçecektir.

Belirli bir işleyici dizisinin elemanları ve sıralarının çalışma anında değişmesi gerekiyorsa chain of responsibility desenini kullanabilirsiniz.

İşleyici sınıfının içinde, bir sonraki işleyicinin referansını tutan alanı değiştirecek ayarlayıcılar dahil ederseniz işleyicileri çalışma anında ekleyebilir, çıkartabilir veya sıralarını değiştirebilirsiniz.

Diğer tasarım desenleri/kalıpları ile ilişkisi

  • Chain of Responsiblity, Command, Mediator ve Observer alıcı ve göndericileri birbirine bağlamak için çeşitli yöntemler önerir
    • Chain of Responsibility bir isteği potansiyel alıcılardan en az biri işleyene kadar dinamik bir potansiyel alıcı zinciri boyunca sırayla iletir.
    • Command göndericiler ve alıcılar arasında tek yönlü bağlantılar kurar.
    • 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.
    • Observer alıcıların isteklere dinamik olarak abone olmalarını ve abonelikten çıkmalarını sağlar.
  • Chain of Responsibility genellikle Composite ile birlikte kullanılır. Bu durumda bir yaprak bileşen istek aldığında, onu tüm ana bileşenlerin zincirinden geçirerek nesne ağacının köküne kadar iletebilir.
  • Chain of Responsibility ‘deki işleyiciler (handler) Command olarak uygulanabilir. Bu durumda farklı bir çok işlem, istekle ilgili bilgiler içeren aynı bağlam (context) nesnesi üzerinde çalıştırılır.

    Öte yandan isteğin kendisinin bir Command nesnesi olduğu bir yaklaşımda mevcuttur. Bu durumda aynı işlemi bir zincirdeki farklı bağlam ( context ) nesnelerinin tamamında çalıştırabilirsiniz.
  • Chain of Responsibility ve Decorator çok benzer sınıf (class) yapılarına sahiptir. Her iki desende işlemi bir dizi nesneye aktarmak için öz yinelemeli (recursive) bir yapı kullanır. Fakat bazı önemi farklar mevcuttur;

    Chain of Responsibility işleyicileri işlemleri birbirinden bağımsız olarak yürütebilir ve herhangi bir sebeple işlemi sonraki aşamalara aktarmayabilir. Decoratorler ise nesnenin davranışını, temel ara yüzü (interface) ile uyumlu olacak şekilde genişletirler. Ayrıca dekoratörler isteğin akışını kesemezler.

Chain of Responsibility Tasarım Deseni Kod Örnekleri

Örnek PHP Kodu

<?php

namespace RefactoringGuru\ChainOfResponsibility\Conceptual;

/**
 * The Handler interface declares a method for building the chain of handlers.
 * It also declares a method for executing a request.
 */
interface Handler
{
    public function setNext(Handler $handler): Handler;

    public function handle(string $request): ?string;
}

/**
 * The default chaining behavior can be implemented inside a base handler class.
 */
abstract class AbstractHandler implements Handler
{
    /**
     * @var Handler
     */
    private $nextHandler;

    public function setNext(Handler $handler): Handler
    {
        $this->nextHandler = $handler;
        // Returning a handler from here will let us link handlers in a
        // convenient way like this:
        // $monkey->setNext($squirrel)->setNext($dog);
        return $handler;
    }

    public function handle(string $request): ?string
    {
        if ($this->nextHandler) {
            return $this->nextHandler->handle($request);
        }

        return null;
    }
}

/**
 * All Concrete Handlers either handle a request or pass it to the next handler
 * in the chain.
 */
class MonkeyHandler extends AbstractHandler
{
    public function handle(string $request): ?string
    {
        if ($request === "Banana") {
            return "Monkey: I'll eat the " . $request . ".\n";
        } else {
            return parent::handle($request);
        }
    }
}

class SquirrelHandler extends AbstractHandler
{
    public function handle(string $request): ?string
    {
        if ($request === "Nut") {
            return "Squirrel: I'll eat the " . $request . ".\n";
        } else {
            return parent::handle($request);
        }
    }
}

class DogHandler extends AbstractHandler
{
    public function handle(string $request): ?string
    {
        if ($request === "MeatBall") {
            return "Dog: I'll eat the " . $request . ".\n";
        } else {
            return parent::handle($request);
        }
    }
}

/**
 * The client code is usually suited to work with a single handler. In most
 * cases, it is not even aware that the handler is part of a chain.
 */
function clientCode(Handler $handler)
{
    foreach (["Nut", "Banana", "Cup of coffee"] as $food) {
        echo "Client: Who wants a " . $food . "?\n";
        $result = $handler->handle($food);
        if ($result) {
            echo "  " . $result;
        } else {
            echo "  " . $food . " was left untouched.\n";
        }
    }
}

/**
 * The other part of the client code constructs the actual chain.
 */
$monkey = new MonkeyHandler();
$squirrel = new SquirrelHandler();
$dog = new DogHandler();

$monkey->setNext($squirrel)->setNext($dog);

/**
 * The client should be able to send a request to any handler, not just the
 * first one in the chain.
 */
echo "Chain: Monkey > Squirrel > Dog\n\n";
clientCode($monkey);
echo "\n";

echo "Subchain: Squirrel > Dog\n\n";
clientCode($squirrel);

Örnek Python Kodu

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Optional


class Handler(ABC):
    """
    The Handler interface declares a method for building the chain of handlers.
    It also declares a method for executing a request.
    """

    @abstractmethod
    def set_next(self, handler: Handler) -> Handler:
        pass

    @abstractmethod
    def handle(self, request) -> Optional[str]:
        pass


class AbstractHandler(Handler):
    """
    The default chaining behavior can be implemented inside a base handler
    class.
    """

    _next_handler: Handler = None

    def set_next(self, handler: Handler) -> Handler:
        self._next_handler = handler
        # Returning a handler from here will let us link handlers in a
        # convenient way like this:
        # monkey.set_next(squirrel).set_next(dog)
        return handler

    @abstractmethod
    def handle(self, request: Any) -> str:
        if self._next_handler:
            return self._next_handler.handle(request)

        return None


"""
All Concrete Handlers either handle a request or pass it to the next handler in
the chain.
"""


class MonkeyHandler(AbstractHandler):
    def handle(self, request: Any) -> str:
        if request == "Banana":
            return f"Monkey: I'll eat the {request}"
        else:
            return super().handle(request)


class SquirrelHandler(AbstractHandler):
    def handle(self, request: Any) -> str:
        if request == "Nut":
            return f"Squirrel: I'll eat the {request}"
        else:
            return super().handle(request)


class DogHandler(AbstractHandler):
    def handle(self, request: Any) -> str:
        if request == "MeatBall":
            return f"Dog: I'll eat the {request}"
        else:
            return super().handle(request)


def client_code(handler: Handler) -> None:
    """
    The client code is usually suited to work with a single handler. In most
    cases, it is not even aware that the handler is part of a chain.
    """

    for food in ["Nut", "Banana", "Cup of coffee"]:
        print(f"\nClient: Who wants a {food}?")
        result = handler.handle(food)
        if result:
            print(f"  {result}", end="")
        else:
            print(f"  {food} was left untouched.", end="")


if __name__ == "__main__":
    monkey = MonkeyHandler()
    squirrel = SquirrelHandler()
    dog = DogHandler()

    monkey.set_next(squirrel).set_next(dog)

    # The client should be able to send a request to any handler, not just the
    # first one in the chain.
    print("Chain: Monkey > Squirrel > Dog")
    client_code(monkey)
    print("\n")

    print("Subchain: Squirrel > Dog")
    client_code(squirrel)

Diğer Tasarım Kalıpları/Design Patterns

Yaratımsal Kalıplar (Creational Patterns)

Yapısal Kalıplar (Structural Patterns)

Davranışsal Kalıplar (Behavioral Patterns)