如何在C#/.NET Core中使用責任鏈模式

LamondLu發表於2020-05-07

原文:Chain Of Responsbility Pattern In C#/.NET Core
作者:Wade
譯者:Lamond Lu

最近我有一個朋友在研究經典的“Gang Of Four”設計模式。他經常來詢問我在實際業務應用中使用了哪些設計模式。單例模式、工廠模式、中介者模式 - 都是我之前使用過,甚至寫過相關文章的模式。但是有一種模式是我還沒有寫過文章,即責任鏈模式。

什麼是責任鏈?

責任鏈模式(之前我經常稱之為命令鏈模式)是一種允許以使用分層方式”處理“物件的模式。在維基百科中的經典定義是

在物件導向設計中,責任鏈模式是一種由命令物件源及其一系列處理物件組成的設計模式。每個處理物件包含了它可以處理的命令物件的邏輯,其餘的將傳遞給鏈中的下一個處理物件。當然,這裡還存在一種將新的處理物件追加到鏈尾的機制。因此責任鏈是If..else if.. else if...else...endif的物件導向版本。其優點是可以在執行時動態重新排列或配置條件操作塊。

也許你會覺著上面的概念描述過於抽象,不容易理解,那麼下面讓我們來看一個真實生活中的例子。

這裡假設我們擁有一家銀行,銀行裡面有3個級別的員工,分別是“櫃員”、“主管”、“銀行經理”。如果有人來取款,“櫃員”只允許10,000美元以下的取款操作。如果金額超過10,000美元,那麼它的請求將傳遞給“主管”。“主管”可以處理不超過100,000美元的請求,但前提是該賬戶在必須有身份證ID。如果沒有身份證ID,則當前請求必須被拒絕。如果取款金額超過100,000美元,則當前請求可以轉交給“銀行經理”,“銀行經理”可以批准任何取款金額,因為如果有人取超過100,000美元的金額,他們就是VIP, 我們不在乎VIP的身份證ID和其他規定。

這就是我們前面討論的分層“鏈”,每個人都嘗試處理當前請求,如果沒有滿足要求,就傳遞給下一個。如果我們將這種場景轉換成程式碼,就是我們所說的責任鏈模式。但是在這之前,讓我們先來看一個糟糕的實現方法。

一個糟糕的實現方式

下面我們先使用If/Else塊來解決當前問題。

class BankAccount
{
    bool idOnRecord { get; set; }

    void WithdrawMoney(decimal amount)
    {
        // 櫃員處理
        if(amount < 10000)
        {
            Console.WriteLine("櫃員提取的金額");
        } 
        // 主管處理
        else if (amount < 100000)
        {
            if(!idOnRecord)
            {
                throw new Exception("客戶沒有身份證ID");
            }

            Console.WriteLine("主管提取的金額");
        }
        else
        {
            Console.WriteLine("銀行經理提取的金額");
        }
    }
}

以上這種實現方式有幾個問題:

  • 新增一種新的員工級別會相當困難,因為IF/Else程式碼塊看起來太亂了
  • “主管”檢查身份證ID的邏輯在某種程度上很難進行單元測試,因為它必須首先通過其他的檢查
  • 雖然現在我們只定義了提款金額的邏輯,但是如果在將來我們想要新增其他檢查(例如:VIP客戶始終由主管來處理), 這種邏輯將很難管理,並且很容易失控。

使用責任鏈模式編碼

下面讓我們重寫一些這部分程式碼。與之前不同,這裡我們建立一些“員工”物件,裡面封裝了他們的處理邏輯。這裡最重要的是,我們需要給每個員工物件指定一個直屬上級,以便當他們處理不了當前請求的時候,可以將請求傳遞給直屬上級。

interface IBankEmployee
{
    IBankEmployee LineManager { get; }
    void HandleWithdrawRequest(BankAccount account, decimal amount);
}

class Teller : IBankEmployee
{
    public IBankEmployee LineManager { get; set; }

    public void HandleWithdrawRequest(BankAccount account, decimal amount)
    {
        if(amount > 10000)
        {
            LineManager.HandleWithdrawRequest(account, amount);
            return;
        }

        Console.WriteLine("櫃員提取的金額");
    }
}

class Supervisor : IBankEmployee
{
    public IBankEmployee LineManager { get; set; }

    public void HandleWithdrawRequest(BankAccount account, decimal amount)
    {
        if (amount > 100000)
        {
            LineManager.HandleWithdrawRequest(account, amount);
            return;
        }

        if(!account.idOnRecord)
        {
            throw new Exception("客戶沒有身份證ID");
        }

        Console.WriteLine("主管提取的金額");
    }
}

class BankManager : IBankEmployee
{
    public IBankEmployee LineManager { get; set; }

    public void HandleWithdrawRequest(BankAccount account, decimal amount)
    {
        Console.WriteLine("銀行經理提取的金額");
    }
}

我們可以通過指定上級的方式建立出責任鏈。這看起來很像一個組織結構圖。

var bankManager = new BankManager();
var bankSupervisor = new Supervisor { LineManager = bankManager };
var frontLineStaff = new Teller { LineManager = bankSupervisor };

這裡我們可以建立一個BankAccount類,並將取款方法轉換為由前臺員工處理。

class BankAccount
{
    public bool idOnRecord { get; set; }

    public void WithdrawMoney(IBankEmployee frontLineStaff, decimal amount)
    {
         frontLineStaff.HandleWithdrawRequest(this, amount);
    }
}

現在,當我們進行取款請求的時候,“櫃員”總是第一個來處理,如果處理不了,它會自動將請求發給直屬領導。這種模式的優雅之處有以下幾點:

  • 鏈中的後續子項並不需要知道是哪個子項將命令傳遞給它的。就像這裡,“主管”不需要知道是為什麼下級“櫃員”為什麼會把請求傳遞給他
  • "櫃員"不需要知道整個鏈。他僅負責將請求傳遞給上級""主管"",期望請求能在上級“主管”那裡被處理(當前也許還需要進一步的傳遞處理)即可
  • 當引入新員工型別的時候,整個組織架構圖很容易變更。例如, 我建立了一個新的“櫃員經理”角色,他能處理10,000-50,000美元之間的提款請求,“櫃員經理”的直屬上級是“主管”。這裡我們並不需要對“主管”物件做任何的處理,只需要將“櫃員”的直屬上級改為“櫃員經理”即可
  • 當編寫單元測試的時候,我們可以一次只關注一個僱員角色了。例如,在測試“主管”邏輯的時候,我們就不需要測試“櫃員”的邏輯了

擴充套件我們的例子

儘管我認為以上的例子已經能很好的說明這種模式,但是通常你會發現有些人會使用一個方法叫做SetNext.一般來說,我覺著這在C#中是非常罕見的,因為C#中我們可以使用屬性獲取器和設定器。使用SetVariableName方法通常都是C++時代的事情了,那時候這通常是封裝變數的首選方法。

但這裡最重要的是,其他示例通常使用抽象類來加強請求傳遞的方式。在前面程式碼中有一個問題是,將請求傳遞給下一個處理器的時候,編寫了許多重複程式碼。那麼就讓我們來整理一下程式碼。

這裡我們要做的第一件事情就是建立一個抽象類,這個抽象類使我們能夠通過標準化的方式處理提款請求。它應該定義一個檢測條件,如果條件滿足,就執行提款,反之,就將請求傳遞給直屬上級。經過修改之後的程式碼如下:

interface IBankEmployee
{
    IBankEmployee LineManager { get; }
    void HandleWithdrawRequest(BankAccount account, decimal amount);
}

abstract class BankEmployee : IBankEmployee
{
    public IBankEmployee LineManager { get; private set; }

    public void SetLineManager(IBankEmployee lineManager)
    {
        this.LineManager = lineManager;
    }

    public void HandleWithdrawRequest(BankAccount account, decimal amount)
    {
        if (CanHandleRequest(account, amount))
        {
            Withdraw(account, amount);
        } 
        else
        {
            LineManager.HandleWithdrawRequest(account, amount);
        }
    }

    abstract protected bool CanHandleRequest(BankAccount account, decimal amount);

    abstract protected void Withdraw(BankAccount account, decimal amount);
}

下一步,我們需要修改所有的員工類,使其繼承自BankEmployee抽象類

class Teller : BankEmployee, IBankEmployee
{
    protected override bool CanHandleRequest(BankAccount account, decimal amount)
    {
        if (amount > 10000)
        {
            return false;
        }
        return true;
    }

    protected override void Withdraw(BankAccount account, decimal amount)
    {
        Console.WriteLine("櫃員提取的金額");
    }
}

class Supervisor : BankEmployee, IBankEmployee
{
    protected override bool CanHandleRequest(BankAccount account, decimal amount)
    {
        if (amount > 100000)
        {
            return false;
        }
        return true;
    }

    protected override void Withdraw(BankAccount account, decimal amount)
    {
        if (!account.idOnRecord)
        {
            throw new Exception("客戶沒有身份證ID");
        }

        Console.WriteLine("主管提取的金額");
    }
}

class BankManager : BankEmployee, IBankEmployee
{
    protected override bool CanHandleRequest(BankAccount account, decimal amount)
    {
        return true;
    }

    protected override void Withdraw(BankAccount account, decimal amount)
    {
        Console.WriteLine("銀行經理提取的金額");
    }
}

這裡請注意,在所有的場景中,都會呼叫抽象類中的HandleWithdrawRequest公共方法。 該方法會呼叫子類中定義的CanHandleRequest方法來檢測當前角色是否滿足處理請求的條件,如果滿足,就呼叫子類中的Withdraw方法處理請求,否則就會嘗試將請求傳遞給上級角色。

我們只需要像以下程式碼這樣,更改建立員工鏈的方式即可:

var bankManager = new BankManager();

var bankSupervisor = new Supervisor();
bankSupervisor.SetLineManager(bankManager);

var frontLineStaff = new Teller();
frontLineStaff.SetLineManager(bankSupervisor);

這裡我需要再次重申,我並不喜歡使用SetXXX這種方法,但是許多例子中都喜歡這麼使用,所以我就把它加了進來。

在一些例子中,也會將判斷員工是否滿足處理請求的條件放在抽象類中。我個人不喜歡這樣做,因為這意味著所有的處理程式不得不使用相似的邏輯。例如,目前所有的檢查都是基於提取金額的,但是如果我們想要實現一個特殊的處理程式,它的條件和VIP標誌有關,那麼我們將不得不又在抽象類中重新使用IF/Else, 這又將我們帶回到了IF/Else地獄中。

什麼時候應該使用責任鏈模式?

這種模式最佳的使用場景是,你的業務上有一個邏輯上的處理鏈,這個處理鏈每次必須按照順序執行。這裡請注意,鏈分叉是這種模式的一個變體, 但是很快處理起來就會非常複雜。因此,當我對現實世界中“命令鏈”場景建模的時候,我通常會使用這種模式。這就是我以銀行為例的原因,因為它就是現實世界中可以用程式碼建模的“責任鏈”。

相關文章