設計模式的征途—14.職責鏈(Chain of Responsibility)模式

Edison Chou發表於2017-07-23

相信大家都玩過類似於“鬥地主”的紙牌遊戲,某人出牌給他的下家,下家看看手中的牌,如果要不起,則將出牌請求轉發給他的下家,其下家再進行判斷。一個迴圈下來,如果其他人都要不起該牌,則最初的出牌者可以打出新牌。在這個過程中,紙牌作為一個請求沿著一條鏈在傳遞,每一位紙牌的玩家都可以處理該請求。在設計模式中,也有一種專門用於處理這種請求鏈式的模式,它就是職責鏈模式。

職責鏈模式(Chain of Responsibility) 學習難度:★★★☆☆ 使用頻率:★★☆☆☆

一、採購單的分級審批模組設計

需求背景:M公司承接了某企業SCM(Supply Chain Management,供應鏈管理)系統的開發任務,其中包含一個採購審批子系統。該企業的採購審批是分級進行的,即根據採購金額的不同由不同層次的主管人員來審批:主任可以審批5萬元以下(不包括5萬)的採購單,副董事長可以審批5萬~10萬(不包括10萬)的採購單,50萬元以及以上的採購單就需要開董事會討論決定,如下圖所示:

  M公司開發人員提出了一個初始解決方案,提供了一個採購單處理類PurchaseRequestHandler用於統一處理採購單,其框架程式碼如下:

    /// <summary>
    /// 採購單處理類
    /// </summary>
    public class PurchaseRequestHandler
    {
        // 遞交採購單給審批者
        public void SendRequestToApprover(PurchaseRequest request)
        {
            if (request.Amount < 5000) // 主任可審批該採購單
            {
                HandleByDirector(request);
            }
            else if(request.Amount < 100000) // 副董事長可審批該採購單
            {
                HandleByVicePresident(request);
            }
            else if (request.Amount < 500000) // 董事長可審批該採購單
            {
                HandleByPresident(request);
            }
            else
            {
                HandleByCongress(request); // 董事會可審批該採購單
            }
        }

        // 主管審批採購單
        private void HandleByDirector(PurchaseRequest request)
        {
            // 程式碼省略
        }

        // 副董事長審批採購單
        private void HandleByVicePresident(PurchaseRequest request)
        {
            // 程式碼省略
        }

        // 董事長審批採購單
        private void HandleByPresident(PurchaseRequest request)
        {
            // 程式碼省略
        }

        // 董事會審批採購單
        private void HandleByCongress(PurchaseRequest request)
        {
            // 程式碼省略
        }
    }

  不過仔細分析後發現,上述方案存在以下3個問題:

  (1)PurchaseRequestHandler類較為龐大,各個級別的審批方法都集中在一個類中,違反了單一職責原則,測試和維護難度較大。

  (2)如果需要新增一個新的審批級別或調整任何一級的審批金額和審批細節時都必須修改原始碼並進行嚴格測試。此外,如果需要移除某一級別時也需要對原始碼進行修改,違反了開閉原則。

  (3)審批流程的設定缺乏靈活性,現在的審批流程是“主任->副董事長->董事長->董事會”,如果需要改為“主任->董事長->董事會”,在此方案中只能通過修改原始碼來實現,客戶端無法定製審批流程。

  那麼如何破呢?別急,來看看職責鏈模式。

二、職責鏈模式概述

2.1 職責鏈模式簡介

職責鏈(Chain of Responsibility)模式:避免將請求傳送者與接受者耦合在一起,讓多個物件都有機會接受請求,將這些物件連成一條鏈,並且沿著這條鏈傳遞請求,直到有物件處理它為止。職責鏈模式是一種物件行為型模式。  

2.2 職責鏈模式結構

  職責鏈模式結構的核心就在於引入了一個抽象處理者,其結構如下圖所示:

  在職責鏈模式結構圖中包含以下兩個角色:

  (1)Handler(抽象處理者):定義了一個處理請求的介面,一般設計為抽象類,由於不同的具體處理者處理請求的方式不同,因此在其中定義了抽象請求處理方法。

  (2)ConcreteHandler(具體處理者):它是抽象處理者的子類,可以處理使用者請求,它實現了在抽象處理者中定義的抽象請求處理方法。在處理請求之前需要判斷是否有相應的處理許可權,如果可以則處理,否則則將請求轉發給後繼者。

三、重構採購單分級審批模組

3.1 重構後的設計

  其中,抽象類Approver充當抽象處理類,Director, VicePresident, President以及Congress 充當具體處理者,PurchaseRequest充當請求類。

3.2 具體程式碼實現

  (1)請求類:PurchaseRequest

    /// <summary>
    /// 採購單:請求類
    /// </summary>
    public class PurchaseRequest
    {
        // 採購金額
        public double Amount { get; set; }
        // 採購單編號
        public string Number { get; set; }
        // 採購目的
        public string Purpose { get; set; }

        public PurchaseRequest(double amount, string number, string purpose)
        {
            Amount = amount;
            Number = number;
            Purpose = purpose;
        }
    }

  (2)抽象處理者:Approver

    /// <summary>
    /// 審批者類:抽象處理者
    /// </summary>
    public abstract class Approver
    {
        protected Approver successor; // 定義後繼物件
        protected string name;  // 審批者姓名

        public Approver(string name)
        {
            this.name = name;
        }

        // 設定後繼者
        public void SetSuccessor(Approver successor)
        {
            this.successor = successor;
        }

        // 抽象請求處理方法
        public abstract void ProcessRequest(PurchaseRequest request);
    }

  (3)具體處理者:Director, VicePresident, President以及Congress 

    /// <summary>
    /// 總監:具體處理類
    /// </summary>
    public class Director : Approver
    {
        public Director(string name) : base(name)
        {
        }

        // 具體請求處理方法
        public override void ProcessRequest(PurchaseRequest request)
        {
            if (request.Amount < 50000)
            {
                // 處理請求
                Console.WriteLine("主管 {0} 審批採購單:{1},金額:{2} 元,採購目的:{3}。",
                    this.name, request.Number, request.Amount, request.Purpose);
            }
            else
            {
                // 如果處理不了,轉發請求給更高層領導
                this.successor.ProcessRequest(request);
            }
        }
    }

    /// <summary>
    /// 副總裁:具體處理類
    /// </summary>
    public class VicePresident : Approver
    {
        public VicePresident(string name) : base(name)
        {
        }

        // 具體請求處理方法
        public override void ProcessRequest(PurchaseRequest request)
        {
            if (request.Amount < 100000)
            {
                // 處理請求
                Console.WriteLine("副總裁 {0} 審批採購單:{1},金額:{2} 元,採購目的:{3}。",
                    this.name, request.Number, request.Amount, request.Purpose);
            }
            else
            {
                // 如果處理不了,轉發請求給更高層領導
                this.successor.ProcessRequest(request);
            }
        }
    }

    /// <summary>
    /// 總裁:具體處理者
    /// </summary>
    public class President : Approver
    {
        public President(string name) : base(name)
        {
        }

        // 具體請求處理方法
        public override void ProcessRequest(PurchaseRequest request)
        {
            if (request.Amount < 500000)
            {
                // 處理請求
                Console.WriteLine("總裁 {0} 審批採購單:{1},金額:{2} 元,採購目的:{3}。",
                    this.name, request.Number, request.Amount, request.Purpose);
            }
            else
            {
                // 如果處理不了,轉發請求給更高層領導
                this.successor.ProcessRequest(request);
            }
        }
    }

    /// <summary>
    /// 董事會:具體處理者
    /// </summary>
    public class Congress : Approver
    {
        public Congress(string name) : base(name)
        {
        }

        // 具體請求處理方法
        public override void ProcessRequest(PurchaseRequest request)
        {
            // 處理請求
            Console.WriteLine("董事會 {0} 審批採購單:{1},金額:{2} 元,採購目的:{3}。",
                this.name, request.Number, request.Amount, request.Purpose);
        }
    }

  (4)客戶端測試:

    public class Program
    {
        public static void Main(string[] args)
        {
            // 建立職責鏈
            Approver andy = new Director("Andy");
            Approver jacky = new VicePresident("Jacky");
            Approver ashin = new President("Ashin");
            Approver meeting = new Congress("Congress");

            andy.SetSuccessor(jacky);
            jacky.SetSuccessor(ashin);
            ashin.SetSuccessor(meeting);
            // 構造採購請求單併傳送審批請求
            PurchaseRequest request1 = new PurchaseRequest(45000.00,
                "MANULIFE201706001",
                "購買PC和顯示器");
            andy.ProcessRequest(request1);

            PurchaseRequest request2 = new PurchaseRequest(60000.00,
                "MANULIFE201706002",
                "2017開發團隊活動");
            andy.ProcessRequest(request2);

            PurchaseRequest request3 = new PurchaseRequest(160000.00,
                "MANULIFE201706003",
                "2017公司年度旅遊");
            andy.ProcessRequest(request3);

            PurchaseRequest request4 = new PurchaseRequest(800000.00,
                "MANULIFE201706004",
                "租用新臨時辦公樓");
            andy.ProcessRequest(request4);

            Console.ReadKey();
        }
    }

  編譯執行後的結果如下圖所示:

  

3.3 需求擴充套件實現

  這時,假設需要在系統中新增一個新的具體處理者,例如增加一個經理(Manager)角色可以審批5萬~8萬(不包括8萬)的採購單。因此,我們可以新增一個具體處理者:Manager

    /// <summary>
    /// 經理:具體處理者
    /// </summary>
    public class Manager : Approver
    {
        public Manager(string name) : base(name)
        {
        }

        // 具體請求處理方法
        public override void ProcessRequest(PurchaseRequest request)
        {
            if (request.Amount < 80000)
            {
                // 處理請求
                Console.WriteLine("經理 {0} 審批採購單:{1},金額:{2} 元,採購目的:{3}。",
                    this.name, request.Number, request.Amount, request.Purpose);
            }
            else
            {
                this.successor.ProcessRequest(request);
            }
        }
    }

  由於鏈的建立過程由客戶端負責,因此此擴充套件對原有類庫無任何影響,符合開閉原則。而我們需要做的,僅僅是在客戶端程式碼中新增職責鏈關係的建立即可。

    public class Program
    {
        public static void Main(string[] args)
        {
            // 建立職責鏈
            Approver andy = new Director("Andy");
            Approver jacky = new Manager("Jacky");
            Approver ashin = new VicePresident("Ashin");
            Approver anya = new President("Anya");
            Approver meeting = new Congress("Congress");

            andy.SetSuccessor(jacky);
            jacky.SetSuccessor(ashin);
            ashin.SetSuccessor(anya);
            anya.SetSuccessor(meeting);
            // 構造採購請求單併傳送審批請求
            PurchaseRequest request1 = new PurchaseRequest(45000.00,
                "MANULIFE201706001",
                "購買PC和顯示器");
            andy.ProcessRequest(request1);

            PurchaseRequest request2 = new PurchaseRequest(60000.00,
                "MANULIFE201706002",
                "2017開發團隊活動");
            andy.ProcessRequest(request2);

            PurchaseRequest request3 = new PurchaseRequest(160000.00,
                "MANULIFE201706003",
                "2017公司年度旅遊");
            andy.ProcessRequest(request3);

            PurchaseRequest request4 = new PurchaseRequest(800000.00,
                "MANULIFE201706004",
                "租用新臨時辦公樓");
            andy.ProcessRequest(request4);

            Console.ReadKey();
        }
    }

  重新編譯執行後的結果如下圖所示:

  

四、職責鏈模式總結

4.1 主要優點

  (1)使得一個物件無需知道是其他哪一個物件處理其請求,物件僅需知道該請求會被處理即可,且鏈式結構由客戶端建立 => 降低了系統的耦合度

  (2)在系統中增加一個新的具體處理者無須修改原有系統原始碼,只需要在客戶端重新建立鏈式結構即可 => 符合開閉原則

4.2 主要缺點

  (1)由於一個請求沒有一個明確地接受者 => 無法保證它一定會被處理

  (2)對於較長的職責鏈 => 系統效能有一定影響且不利於除錯

  (3)如果建立鏈不當,可能會造成迴圈呼叫 => 導致系統進入死迴圈

4.3 應用場景

  (1)有多個物件處理同一個請求且無需關心請求的處理物件時誰以及它是如何處理的 => 比如各種審批流程

  (2)可以動態地指定一組物件處理請求,客戶端可以動態建立職責鏈來處理請求,還可以改變鏈中處理者之間的先後次序 => 比如各種流程定製

參考資料

  DesignPattern

  劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》

 

相關文章