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

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

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

什麼是責任鏈?

責任鏈模式(以前我常常稱之爲命令鏈模式)是一種容許以使用分層方式」處理「對象的模式。在維基百科中的經典定義是架構

在面向對象設計中,責任鏈模式是一種由命令對象源及其一系列處理對象組成的設計模式。每一個處理對象包含了它能夠處理的命令對象的邏輯,其他的將傳遞給鏈中的下一個處理對象。固然,這裏還存在一種將新的處理對象追加到鏈尾的機制。所以責任鏈是If..else if.. else if...else...endif的面向對象版本。其優勢是能夠在運行時動態從新排列或配置條件操做塊。ide

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

這裏假設咱們擁有一家銀行,銀行裏面有3個級別的員工,分別是「櫃員」、「主管」、「銀行經理」。若是有人來取款,「櫃員」只容許10,000美圓如下的取款操做。若是金額超過10,000美圓,那麼它的請求將傳遞給「主管」。「主管」能夠處理不超過100,000美圓的請求,但前提是該帳戶在必須有身份證ID。若是沒有身份證ID,則當前請求必須被拒絕。若是取款金額超過100,000美圓,則當前請求能夠轉交給「銀行經理」,「銀行經理」能夠批准任何取款金額,由於若是有人取超過100,000美圓的金額,他們就是VIP, 咱們不在意VIP的身份證ID和其餘規定。測試

這就是咱們前面討論的分層「鏈」,每一個人都嘗試處理當前請求,若是沒有知足要求,就傳遞給下一個。若是咱們將這種場景轉換成代碼,就是咱們所說的責任鏈模式。可是在這以前,讓咱們先來看一個糟糕的實現方法。this

一個糟糕的實現方式

下面咱們先使用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客戶始終由主管來處理), 這種邏輯將很難管理,而且很容易失控。

使用責任鏈模式編碼

下面讓咱們重寫一些這部分代碼。與以前不一樣,這裏咱們建立一些「員工」對象,裏面封裝了他們的處理邏輯。這裏最重要的是,咱們須要給每一個員工對象指定一個直屬上級,以便當他們處理不了當前請求的時候,能夠將請求傳遞給直屬上級。code

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地獄中。

何時應該使用責任鏈模式?

這種模式最佳的使用場景是,你的業務上有一個邏輯上的處理鏈,這個處理鏈每次必須按照順序運行。這裏請注意,鏈分叉是這種模式的一個變體, 可是很快處理起來就會很是複雜。所以,當我對現實世界中「命令鏈」場景建模的時候,我一般會使用這種模式。這就是我以銀行爲例的緣由,由於它就是現實世界中能夠用代碼建模的「責任鏈」。

相關文章
相關標籤/搜索