14. Chain of Responsibility


Posted by WayneCheng on 2021-01-21

關於 Chain of Responsibility 本篇將討論以下幾個問題

1. 關於 Chain of Responsibility

2. UML

3. 將 UML 轉為程式碼

4. 情境


測試環境:

OS:Windows 10
IDE:Visual Studio 2019


1. 關於 Chain of Responsibility

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.

by Gang of Four

  • 藉由給一個以上對像處理請求的機會,避免將請求的發送者與接收者耦合
  • 鏈接接收對象,並將請求沿著鏈傳遞,直到對象處理該請求

Chain of Responsibility(責任鏈)屬於行為型(Behavioral Patterns),當遇到未知請求 & 未知處理者時,可使用 Chain of Responsibility 來將請求依序對應全部處理者,找尋可處理請求的對象。當遇到過濾、攔截的需求時,可使用 Chain of Responsibility 來依照指定順序執行判斷邏輯。

優點:

  • 符合 單一職責原則(Single Responsibility Principle)
  • 符合 開閉原則(Open Closed Principle)

缺點:

  • 用於未知請求 & 未知處理者時,可能會因為處理者過多而影響效能

2. UML

Class 間關聯:

  • Client 關聯 Handler
  • ConcreteHandler 1 / 2 繼承 Handler
  • ConcreteHandler2 可包含 Handler

Class:

  • Client:呼叫端
  • Handler:處理請求的介面
  • ConcreteHandler:處理請求實作

3. 將 UML 轉為程式碼

處理請求的抽象類別

/// <summary>
/// 處理請求的抽象類別
/// </summary>
public abstract class Handler
{
    protected Handler successor;

    public void SetSuccessor(Handler successor)
    {
        this.successor = successor;
    }

    public abstract void HandleRequest(int request);
}

處理請求實作 1 / 2 / 3

/// <summary>
/// 處理請求實作
/// </summary>
public class ConcreteHandler1 : Handler
{
    public override void HandleRequest(int request)
    {
        if (request >= 0 && request < 10)
        {
            Console.WriteLine($"{GetType().Name} handled request {request}");
        }
        else if (successor != null)
        {
            successor.HandleRequest(request);
        }
    }
}

/// <summary>
/// 處理請求實作
/// </summary>
public class ConcreteHandler2 : Handler
{
    public override void HandleRequest(int request)
    {
        if (request >= 10 && request < 20)
        {
            Console.WriteLine($"{GetType().Name} handled request {request}");
        }
        else if (successor != null)
        {
            successor.HandleRequest(request);
        }
    }
}

/// <summary>
/// 處理請求實作
/// </summary>
public class ConcreteHandler3 : Handler
{
    public override void HandleRequest(int request)
    {
        if (request >= 20 && request < 30)
        {
            Console.WriteLine($"{GetType().Name} handled request {request}");
        }
        else if (successor != null)
        {
            successor.HandleRequest(request);
        }
    }
}
  1. 建立handler
  2. 經由SetSuccessor設置接續執行的handler
  3. 依序傳入測試資料
static void Main(string[] args)
{
    Default.Handler handler1 = new Default.ConcreteHandler1();
    Default.Handler handler2 = new Default.ConcreteHandler2();
    Default.Handler handler3 = new Default.ConcreteHandler3();
    handler1.SetSuccessor(handler2);
    handler2.SetSuccessor(handler3);

    var requests = new[]{ 5, 12, 24 };

    Console.WriteLine("Array elements: 5, 12, 24");

    foreach (var request in requests)
    {
        handler1.HandleRequest(request);
    }

    Console.ReadLine();
}

執行結果

Array elements: 5, 12, 24
ConcreteHandler1 handled request 5
ConcreteHandler2 handled request 12
ConcreteHandler3 handled request 24

4. 情境

我們接到了一個員工請假簽核的需求

  • 三天內(包含三天)店長同意即可
  • 三到七天(包含七天)店長 & 區主管同意
  • 超過七天則要店長 & 區主管 & 總經理同意
  • 假單需依序簽核

請假簽核抽象類別

/// <summary>
/// 請假簽核抽象類別
/// </summary>
public abstract class Leave
{
    protected Leave successor;

    public void SetSuccessor(Leave successor)
    {
        this.successor = successor;
    }

    public abstract void TakeALeave(int days);
}

店長 & 區主管 & 總經理簽核實作

/// <summary>
/// 店長簽核實作
/// </summary>
public class StoreManagerApprove : Leave
{
    public override void TakeALeave(int days)
    {
        if (days <= 3)
        {
            Console.WriteLine($"Store Manager : OK~");
        }
        else if (successor != null)
        {
            Console.WriteLine($"Store Manager : OK~");
            successor.TakeALeave(days);
        }
    }
}

/// <summary>
/// 區主管簽核實作
/// </summary>
public class DistrictManagerApprove : Leave
{
    public override void TakeALeave(int days)
    {
        if (days <= 7)
        {
            Console.WriteLine($"District Manager : OK~");
        }
        else if (successor != null)
        {
            Console.WriteLine($"District Manager : OK~");
            successor.TakeALeave(days);
        }
    }
}

/// <summary>
/// 總經理簽核實作
/// </summary>
public class GeneralManagerApprove : Leave
{
    public override void TakeALeave(int days)
    {
        if (days > 7)
        {
            Console.WriteLine($"General Manager : OK~");
        }
    }
}
  1. 建立請假流程handler
  2. 經由SetSuccessor設置接續執行的主管
  3. 依序傳入請假天數(1、5、10 天)
static void Main(string[] args)
{
    Situation.StoreManagerApprove storeManager = new Situation.StoreManagerApprove();
    Situation.DistrictManagerApprove districtManager = new Situation.DistrictManagerApprove();
    Situation.GeneralManagerApprove generalManager = new Situation.GeneralManagerApprove();
    storeManager.SetSuccessor(districtManager);
    districtManager.SetSuccessor(generalManager);

    Console.WriteLine($"請假 1 天");
    storeManager.TakeALeave(1);
    Console.WriteLine($"\n");

    Console.WriteLine($"請假 5 天");
    storeManager.TakeALeave(5);
    Console.WriteLine($"\n");

    Console.WriteLine($"請假 10 天");
    storeManager.TakeALeave(10);
    Console.WriteLine($"\n");

    Console.ReadLine();
}

執行結果

請假 1 天
Store Manager : OK~

請假 5 天
Store Manager : OK~
District Manager : OK~

請假 10 天
Store Manager : OK~
District Manager : OK~
General Manager : OK~

完整程式碼

GitHub:Behavioral_01_ChainOfResponsibility


總結

本篇沒有將 abstract class 改為 interface,同樣是因為覺得 Chain of Responsibility 使用繼承的方式能使子類別更加簡潔,將接續處理的動作放在父類別,子類別只專注在處理邏輯上與決定是否需要呼叫接續者繼續執行。


參考資料

  1. Design Patterns
  2. 大話設計模式
  3. dofactory
  4. Refactoring.Guru

新手上路,若有錯誤還請告知,謝謝


#designpattern #CSharp







Related Posts

PHP & CGI 相關知識

PHP & CGI 相關知識

[Release Notes] 20210204_v1 - Add explore page

[Release Notes] 20210204_v1 - Add explore page

MTR04_0624

MTR04_0624


Comments