引子:加薪非要老總批?#

小菜試用三個月即將轉正,向經理提加薪。流程是:

  • 經理:「我做不了主,幫你向上提一提」 → 找人力資源總監
  • 總監:「沒有先例,這事要等總經理來了再提」 → 找總經理
  • 總經理:「畢業生這麼多,三個月就想加薪不合適」

加薪、請假等請求的處理:不同層級有不同的權限,請求逐級上交,直到有人能處理。

這正是職責鏈模式的典型場景。

第一版:一個 Manager 類負責所有層級#

class Request
{
    public string RequestType { get; set; }
    public string RequestContent { get; set; }
    public int Number { get; set; }
}

class Manager
{
    protected string name;
    public Manager(string name) { this.name = name; }

    public void GetResult(string managerLevel, Request request)
    {
        if (managerLevel == "經理")    { /* 請假 ≤ 2 天才批准 */ }
        else if (managerLevel == "總監") { /* 請假 ≤ 5 天才批准 */ }
        else if (managerLevel == "總經理") { /* 加薪 ≤ 500 批准,> 500 再說吧 */ }
    }
}

問題:

  • 方法過長多條分支判斷——典型壞味道
  • 新增管理者類別(部門經理、副總……)要修改這個類,違反開放-封閉原則
  • 一個類承擔太多職責——違反單一職責原則

職責鏈模式#

職責鏈模式(Chain of Responsibility):使多個物件都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關係。將這些物件連成一條鏈,並沿著這條鏈傳遞該請求,直到有一個物件處理它為止。[DP]

關鍵思想:發出請求的客戶端並不知道哪個物件最終會處理這個請求——系統可以動態地重組責任。

結構#

  • Handler(抽象處理者):定義一個處理請求的介面,並設定後繼者
  • ConcreteHandler(具體處理者):處理它所負責的請求;可訪問後繼者,能處理就處理,否則轉發給後繼者
classDiagram
    class Client
    class Handler {
        <<abstract>>
        #Handler successor
        +SetSuccessor(Handler)
        +HandleRequest(int)*
    }
    class ConcreteHandler1
    class ConcreteHandler2
    class ConcreteHandler3
    Client --> Handler
    Handler <|-- ConcreteHandler1
    Handler <|-- ConcreteHandler2
    Handler <|-- ConcreteHandler3
    Handler --> Handler : successor
sequenceDiagram
    participant 小菜
    participant 經理
    participant 總監
    participant 總經理
    小菜->>經理: 加薪 1000 元申請
    經理->>總監: 我無權處理(轉發)
    總監->>總經理: 我無權處理(轉發)
    總經理-->>小菜: 加薪 > 500,再說吧
abstract class Handler
{
    protected Handler successor;
    public void SetSuccessor(Handler successor) => this.successor = successor;
    public abstract void HandleRequest(int request);
}

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

客戶端:

Handler h1 = new ConcreteHandler1();
Handler h2 = new ConcreteHandler2();
Handler h3 = new ConcreteHandler3();

h1.SetSuccessor(h2);
h2.SetSuccessor(h3);

foreach (int req in new[] { 2, 5, 14, 22, 18 })
    h1.HandleRequest(req);

重構加薪範例#

把原來的單一 Manager 類拆成抽象類 + 三個具體類:

abstract class Manager
{
    protected string name;
    protected Manager superior;
    public Manager(string name) { this.name = name; }
    public void SetSuperior(Manager superior) => this.superior = superior;
    public abstract void RequestApplications(Request request);
}

class CommonManager : Manager // 經理
{
    public CommonManager(string name) : base(name) { }
    public override void RequestApplications(Request request)
    {
        if (request.RequestType == "請假" && request.Number <= 2)
            Console.WriteLine($"{name}: {request.RequestContent} 數量 {request.Number} 被批准");
        else if (superior != null)
            superior.RequestApplications(request);
    }
}

class Majordomo : Manager // 總監
{
    public Majordomo(string name) : base(name) { }
    public override void RequestApplications(Request request)
    {
        if (request.RequestType == "請假" && request.Number <= 5)
            Console.WriteLine($"{name}: {request.RequestContent} 數量 {request.Number} 被批准");
        else if (superior != null)
            superior.RequestApplications(request);
    }
}

class GeneralManager : Manager // 總經理
{
    public GeneralManager(string name) : base(name) { }
    public override void RequestApplications(Request request)
    {
        if (request.RequestType == "請假")
            Console.WriteLine($"{name}: {request.RequestContent} 數量 {request.Number} 被批准");
        else if (request.RequestType == "加薪" && request.Number <= 500)
            Console.WriteLine($"{name}: {request.RequestContent} 數量 {request.Number} 被批准");
        else if (request.RequestType == "加薪" && request.Number > 500)
            Console.WriteLine($"{name}: {request.RequestContent} 數量 {request.Number} 再說吧");
    }
}

客戶端:

CommonManager jinli       = new CommonManager("金利");
Majordomo zongjian        = new Majordomo("宗劍");
GeneralManager zhongjingli = new GeneralManager("鍾精勵");

jinli.SetSuperior(zongjian);
zongjian.SetSuperior(zhongjingli);

Request r = new Request { RequestType = "加薪", RequestContent = "小菜請求加薪", Number = 1000 };
jinli.RequestApplications(r); // 由「經理」發起,但實際處理者由鏈決定

客戶端的所有申請都從「經理」發起,實際處理由具體類別決定,客戶端無需關心

職責鏈的好處#

  • 接收者和發送者都沒有對方的明確資訊
  • 鏈中的物件自己也不知道鏈的結構
  • 物件僅需保持一個指向後繼者的引用,大大降低耦合度[DP]
  • 在客戶端定義鏈的結構,可隨時增加或修改處理請求的結構——增強指派職責的靈活性[DP]

注意事項#

一個請求極有可能到了鏈的末端都得不到處理,或因配置不正確而得不到處理——必須事先考慮全面。

就像現實中郵寄一封信因地址不對最終無法送達一樣。

兩個關鍵點:

  • 事先給每個具體處理者設置上級(後繼者)
  • 每個處理者要正確判斷:自己處理還是「推卸責任」轉給後繼者

結局#

故事的結局:經理跳過總監直接找總經理談,最終為小菜爭取到加薪——

經理改變了職責鏈的結構(跳過總監),這也是職責鏈靈活性的體現。