最近我有一个朋友在研究经典的“Gang Of Four”设计模式。他经常来询问我在实际业务应用中使用了哪些设计模式。单例模式、工厂模式、中介者模式 - 都是我之前使用过,甚至写过相关文章的模式。但是有一种模式是我还没有写过文章,即责任链模式。
什么是责任链?
责任链模式(之前我经常称之为命令链模式)是一种允许以使用分层方式”处理“对象的模式。在维基百科中的经典定义是
也许你会觉着上面的概念描述过于抽象,不容易理解,那么下面让我们来看一个真实生活中的例子。
这里假设我们拥有一家银行,银行里面有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地狱中。
什么时候应该使用责任链模式?
这种模式最佳的使用场景是,你的业务上有一个逻辑上的处理链,这个处理链每次必须按照顺序运行。这里请注意,链分叉是这种模式的一个变体, 但是很快处理起来就会非常复杂。因此,当我对现实世界中“命令链”场景建模的时候,我通常会使用这种模式。这就是我以银行为例的原因,因为它就是现实世界中可以用代码建模的“责任链”。