Anasayfa » 16. Chain of Responsibility Pattern

16. Chain of Responsibility Pattern

by Levent KARAGÖL
13 dakikalık okuma süresi

Bu makalemizde, gelen talebi zincir şeklinde birbirine bağlanmış bir nesne grubuna aktararak işlem yapılmasını sağlayan Chain of Responsibility tasarım kalıbını inceleyeceğiz. Bu serinin genelinde olduğu gibi öncelikle Chain of Responsibility tasarımın kalıbının ne olduğunu, hangi durumlarda hangi sorunlara çözüm sunduğunu konuşacak, sonrasında gelen bir talebe istinaden herhangi bir tasarım kalıbı kullanmadan bir çözüm üretecek, ardından bu çözümün problemlerini ve hangi yazılım tasarım prensiplerine aykırı olduğunu tartışacak, son olarak da Chain of Responsibility tasarım kalıbı ile aynı talebe uygun bir çözüm üretmeye çalışacağız.

Not: Makale genelindeki örneklerin kaynak kodlarına Github üzerinden ulaşabilirsiniz.


Chain of Responsibility Tasarım Kalıbı Nedir?

Klasik tasarım kalıplarından Behavioral Design Patterns kategorisinde yer alan ilk tasarım kalıbımız olan Chain of Responsibility tasarım kalıbı, zincir şeklinde birbiri arkasına eklenmiş nesneler üzerinden talebi geçirerek, zincirdeki her elemanın kendine ait görevi yerine getirmesi (veya pas geçmesi) sonrasında talebi kendinden bir sonraki elemana iletmesi prensibine göre çalışır.

Bu tasarım kalıbında, zincire eklenmiş her bir nesne sadece kendisi ile alakalı kısım hakkında bilgi sahibidir. Dolayısıyla gelen talebe göre sürecin başarıyla tamamlanacağını dahi bilemez. Tek bildiği, kendine geçilen veriye göre çalışıp çalışmaması gerektiğidir. Sonrası bu nesnenin sorumluluğu dışındadır.

Oldukça farklı şekillerde karşımıza çıkan bu tasarım kalıbına güncel kullanım örneği olarak, talep edilen tutara göre üst birim yöneticisine doğru eskale olan avans yönetim formları, tanımlı kurallara göre işleyen ardışık süreç motorları ve hatta DDD (Domain Driven Design) ve microservice mimarilerinde birden fazla domain veya microservice tarafından tamamlanabilecek süreçlerin yönetiminde Application Service veya Aggregator Service katmanında kurulan routing benzeri yapılar verilebilir.

Chain of Responsibility tasarım kalıbı sayesinde, alt birimlerin çalışıp çalışmayacağına karar veren mantıksal yapılar, dışarıdaki alt birimleri çağıran üst katman yerine her bir alt katmanın içine taşınabilir. Böylece iş mantığı domain/service dışarısına taşmamış olur ve Separation of Concerns prensibine bağlı kalınır.


Talebimizi Alalım

ATM benzeri bir kiosk için uygulama geliştiriyoruz. Kiosk üzerinde 100, 50, 20, 10, 5 ve 1 TL’lik banknotları (ve bozuk paraları) saklayan kasalar var. Çekilmek istenen tutara göre GPIO portu üzerinden ilgili kasaya, vermesi gereken banknot adedine ilişkin sinyal göndermemiz gerekiyor.


İlk Çözüm Önerimizi Sunalım

Kiosk üzerinde 6 farklı kasamız var. Talep edilen tutar için en büyükten başlayarak eksilte eksilte ilerleyip her bir kasadan verilmesi gereken banknot adedini hesaplayacağız. TypeScript serisindeki ilgili makalede önerilen “tsconfig.json” dosyasını oluşturarak işe başlayalım.

{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "resolveJsonModule": true,
    "removeComments": true,
    "noUnusedParameters": true,
    "declaration": true,
    "lib": [
      "es2020",
      "esnext.asynciterable",
      "dom"
    ]
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts"
  ]
}


Nesne tabanlı programlama gözüyle bakarsak, her bir kasa ile iletişime geçecek ayrı bir nesnemiz olsa güzel olur. Buna ilişkin “Dispenser” sınıfımızı aşağıdaki şekilde projemize ekleyelim.

export class Dispenser {

    constructor(private banknoteValue: number) {
    }

    dispense(quantity: number): void {

        // Para kasasına sinyal gönderiliyor
        console.log(`Banknot: ${this.banknoteValue}, Adet: ${quantity}`);
    }
}


Gelelim asıl işi yapacak “DispenserManager” isimli sınıfımıza. Bu sınıf her bir banknot çeşidi için bir “Dispenser” nesnesi oluşturacak. Ardından talep edilen tutara göre hangi kasadan ne kadar banknot verilmesi gerektiğini hesap ederek, ilgili “Dispenser” nesnesinin “dispense” metodunu çağıracak.

import {Dispenser} from "./dispenser";

export class DispenserManager {

    private dispenser100: Dispenser;
    private dispenser50: Dispenser;
    private dispenser20: Dispenser;
    private dispenser10: Dispenser;
    private dispenser5: Dispenser;
    private dispenser1: Dispenser;

    constructor() {

        this.dispenser100 = new Dispenser(100);
        this.dispenser50 = new Dispenser(50);
        this.dispenser20 = new Dispenser(20);
        this.dispenser10 = new Dispenser(10);
        this.dispenser5 = new Dispenser(5);
        this.dispenser1 = new Dispenser(1);
    }

    dispense(amount: number): void {

        let remainingAmount = amount;

        // Tutar için gerekli 100'lük banknot sayısı bulunuyor
        const dispenser100Quantity = Math.floor(remainingAmount / 100);

        if (dispenser100Quantity > 0) {

            this.dispenser100.dispense(dispenser100Quantity);

            remainingAmount -= dispenser100Quantity * 100;
        }

        // Kalan tutar için gerekli 50'lik banknot sayısı bulunuyor
        const dispenser50Quantity = Math.floor(remainingAmount / 50);

        if (dispenser50Quantity > 0) {

            this.dispenser50.dispense(dispenser50Quantity);

            remainingAmount -= dispenser50Quantity * 50;
        }

        // Kalan tutar için gerekli 20'lik banknot sayısı bulunuyor
        const dispenser20Quantity = Math.floor(remainingAmount / 20);

        if (dispenser20Quantity > 0) {

            this.dispenser20.dispense(dispenser20Quantity);

            remainingAmount -= dispenser20Quantity * 20;
        }

        // Kalan tutar için gerekli 10'luk banknot sayısı bulunuyor
        const dispenser10Quantity = Math.floor(remainingAmount / 10);

        if (dispenser10Quantity > 0) {

            this.dispenser10.dispense(dispenser10Quantity);

            remainingAmount -= dispenser10Quantity * 10;
        }

        // Kalan tutar için gerekli 5'lik banknot sayısı bulunuyor
        const dispenser5Quantity = Math.floor(remainingAmount / 5);

        if (dispenser5Quantity > 0) {

            this.dispenser5.dispense(dispenser5Quantity);

            remainingAmount -= dispenser5Quantity * 5;
        }

        // Kalan tutar için gerekli 1'lik banknot sayısı bulunuyor
        const dispenser1Quantity = remainingAmount;

        if (dispenser1Quantity > 0) {

            this.dispenser1.dispense(dispenser1Quantity);
        }
    }
}


Yazdığımız kod biraz uzun gözüküyor ama yapısı oldukça basit. Büyükten küçüğe doğru banknot sayısını hesap ederken, her seferinde kalan tutarı bulup bir sonraki banknot ile yoluna devam ediyor.

Son olarak uygulamamızı ayağa kaldıracak ve “DispenserManager” sınıfından talepte bulunacak “app.ts” dosyamızı aşağıdaki içerik ile oluşturabiliriz.

import {DispenserManager} from "./dispenser-manager";

new DispenserManager().dispense(397);


Uygulamamızı derleyip çalıştırdığımızda aşağıdaki gibi bir ekran görüntüsü ile karşılaşırız. Görüldüğü gibi her bir kasa için adet hesaplandı ve bu süreçte 10 TL’lik banknota ihtiyaç duyulmadı.


Genel yapıyı daha net görebilmek adına öneriye ilişkin Class Diagram aşağıdadır.


Peki Bu Çözümün Nesi Yanlış?

DispenserManager” sınıfındaki “dispense” metodunun, döngü kullanılmadan oldukça acemi şekilde yazıldığını bir kenara bırakırsak, buradaki temel sıkıntımız tüm iş mantığının tek bir metotta birikmiş olması.

Burada yazılan kod Single Responsibility veya Separation of Concerns prensiplerine doğrudan aykırıdır diyemeyiz. Yazılım geliştirici, iş mantığını “DispenserManager” sınıfının sorumluluğuna bırakmış olabilir, çünkü şu anda yaptığı başka bir iş yok.

Ancak bu yaklaşım, genelde tek bir metoda çok fazla yük bindirme ve ek geliştirme sonucunda bu iki prensibi çiğneme eğilimindedir. Örneğin her bir kasada sınırlı sayıda banknot olsaydı ve hesaplama sırasında kalan banknot sayısını da sürece dahil etmek zorunda kalsaydık, nasıl bir kod yazardık? Pek ya ATM’lerde olduğu gibi, imkan varsa kalan son 100 TL’yi bozuk olarak verecek bir algoritma eklemek isteseydik kodumuz nereye giderdi?

Elbette tüm bu olasılıklar yaşanmışlık ve deneyimle gelen birikimler sonucu öğreniliyor ancak temel prensip olarak iş mantığını bölümlere (domain) ayırmak ve ihtiyacı mümkün mertebe lokal bölümün dışına çıkmadan çözmek, bizi daha yönetilebilir, modüler bir yapıya götürür ve günün sonunda 2500 satırlık metotlarla boğuşmak zorunda kalmayız.


Nasıl Bir Çözüm Sunulabilirdi?

Bu tarz ardışık işleyen süreçler için Chain of Responsibility Pattern güzel bir öneri olabilir. Bu tasarım kalıbına göre her bir “Dispenser” nesnesini birbiri arkasına eklememiz gerekiyor. İşi biten nesne, kendinden sonra gelen nesneye işi devretmeli. Öncelikle tüm “Dispenser” sınıflarının uygulayacağı “IDispenser” Interface’imizi aşağıdaki şekilde projemize ekleyelim.

export interface IDispenser {
    setNext(dispenser: IDispenser): IDispenser;
    dispense(amount: number): number;
}


Ardından “Dispenser” sınıfımızı aşağıdaki şekilde değiştirelim.

import {IDispenser} from "./idispenser";

export class Dispenser implements IDispenser {
    private nextDispenser?: IDispenser;

    constructor(private banknoteValue: number) {
    }

    setNext(dispenser: IDispenser): IDispenser {
        this.nextDispenser = dispenser;
        return dispenser;
    }

    dispense(amount: number): number {

        if (amount >= this.banknoteValue) {

            // Belirtilen tutar bu kasa için uygun, adet hesaplanıyor
            const quantity = Math.floor(amount / this.banknoteValue);
            const remainingAmount = amount % this.banknoteValue;

            // Para kasasına sinyal gönderiliyor
            console.log(`Banknot: ${this.banknoteValue}, Adet: ${quantity}`);

            // Kalan tutar için işlem zincirde bir sonraki birime yönlendiriliyor
            return this.nextDispenser ? this.nextDispenser.dispense(remainingAmount) : remainingAmount;

        } else {

            // Belirtilen tutar için uygun banknot yok, işlem zincirde bir sonraki birime yönlendiriliyor
            return this.nextDispenser ? this.nextDispenser.dispense(amount) : amount;
        }
    }
}


Burada iş mantığını “Dispenser” sınıfı içerisine taşıdık. Her bir “Dispenser” nesnesi, kendi “dispense” metodu çağırıldığında, kendisi ile ilişkin adedi hesaplıyor. Kalan tutar kendisi ile ilgili olsa da olmasa da, her durumda kendisinden sonraki “Dispenser” nesnesinin “dispense” metodunu çağırıyor.

Kendisinden sonra gelen “Dispenser” nesnesi, sınıfa ait “setNext” metodu ile kendisine bildiriliyor ve eğer bildirilmediyse, bu kendisinin zincirin son halkası olduğu anlamına geliyor.

Bu durumda tüm “Dispenser” nesnelerini birbirine bağlayacak bir üst katmana ihtiyacımız var. “DispenserManager” sınıfımızı aşağıdaki şekilde güncelleyelim.

import {Dispenser} from "./dispenser";

export class DispenserManager {

    private dispenser: Dispenser;

    constructor() {

        this.dispenser = new Dispenser(100);

        this.dispenser
            .setNext(new Dispenser(50))
            .setNext(new Dispenser(20))
            .setNext(new Dispenser(10))
            .setNext(new Dispenser(5))
            .setNext(new Dispenser(1));
    }

    dispense(amount: number): void {

        this.dispenser.dispense(amount);
    }
}


Burada görüldüğü gibi, “DispenserManager” sınıfımızın tek görevi, “Dispenser” nesnelerini zincir gibi birbirleri arkasına eklemek ve gelen talebi zincirin ilk halkasına iletmek olarak kaldı. Bundan sonrasında zincirin her elemanı, kendisi ile ilişkili iş mantığını yürütüp, kendisinden sonra gelen elemanın “dispense” metodunu çağırarak süreci devam ettirecek.

app.ts” dosyamızda herhangi bir değişiklik yapmamıza gerek yok. Uygulamamızı derleyip çalıştırdığımızda aşağıdaki gibi bir ekran görüntüsü ile karşılaşırız. Görüldüğü üzere 10 TL’lik banknottan sorumlu nesne, kalan tutar kendisine uygun olmadığı için kendi içerisindeki iş mantığını işletmeden, gelen tutarı zincirdeki bir sonraki elemana ileterek süreci devam ettirdi.


Genel yapıyı daha net görebilmek adına öneriye ilişkin Class Diagram aşağıdadır.




Böylece Chain of Responsibility tasarım kalıbı konusunun sonuna geldik. Bir sonraki makalemizde Command tasarım kalıbını işleyeceğiz.

Herkese iyi çalışmalar…

İLGİNİZİ ÇEKEBİLİR

Bir Yorum Yaz