在C#/.NET Core中使用责任链模式
原文: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地狱中。
什么时候应该使用责任链模式?
这种模式最佳的使用场景是,你的业务上有一个逻辑上的处理链,这个处理链每次必须按照顺序运行。这里请注意,链分叉是这种模式的一个变体, 但是很快处理起来就会非常复杂。因此,当我对现实世界中“命令链”场景建模的时候,我通常会使用这种模式。这就是我以银行为例的原因,因为它就是现实世界中可以用代码建模的“责任链”。