【译】C#/.Net core中的职责链模式
原文链接:传送门。
我有个朋友最近第一次研究了经典的“Gang Of Four” 设计模式书籍。他随手就问我哪些设计模式我在我的商业程序中用到了,实际上他的想法是:“此刻我正在用这些设计模式”。单例,工厂模式,中介者模式,我已经使用过所有这些设计模式并且之前写过关于它们的内容。但是之前我从没有讨论过的模式是职责链模式。
什么是“职责链模式”?
职责链(有时候我会称之为命令链)模式是一种设计模式,其允许对一个对象的处理在层级顺序下进行,经典的维基百科定义如下:
在面向对象设计中,责任链模式是一种由命令对象源和一系列处理对象组成的设计模式。每一个处理对象都会包含定义了其所能处理的命令对象类型的逻辑;其余的便会传递到链条中的下一个处理对象。也存在让我们可以在链条的末尾添加自己的处理对象的机制。因此,职责链模式是 if … else if … else if ……. else … endif 的面向对象的版本,其可以带来的益处便是 条件-动作处理单元可以在运行时动态的进行重新安排和配置。
维基百科的解释很可能听起来不是很有道理,让我们来看一看真实世界的例子,随后我们可以将其转化为代码。
让我们假设我有一个银行,在这个银行中我有三个级别的员工,一个银行出纳,一个监督员,一个银行主管。如果一个人进来要提取一些存款,银行出纳可以允许任何少于10000美元的提取,不会问任何问题。如果取款金额超过10000美元,它便会被传给监督员进行处理。监督员可以处理上至100,000美元的请求,但必须要这个账户在记录中具有ID。如果ID没有在记录中,那么无论如何请求便会被拒绝。如果请求金额达到了 100,000美元,其便会流转到银行主管。银行主管可以审批任何金额的提取即使ID并不在记录中,因为如果他们提取那个金额,他们便是VIP并且我们不会在乎洗钱规则。
这便是我们之前讨论的层级链,其中每个人都尝试处理这个请求,并且能够把请求传递给下一个处理人。如果我们采取这种实现并且将其映射为代码(以一种优雅的方式),这就是我们所称的职责链模式。但是在我们进行任何深入之前,让我们看看一个坏的解决此问题的方式。
一个不好的实现
让我们只是使用if/else来解决这整个问题
class BankAccount { bool idOnRecord { get; set; } void WithdrawMoney(decimal amount) { // Handled by the teller. if(amount < 10000) { Console.WriteLine("Amount withdrawn by teller"); } // Handled by supervisor else if (amount < 100000) { if(!idOnRecord) { throw new Exception("Account holder does not have ID on record."); } Console.WriteLine("Amount withdrawn by Supervisor"); } else { Console.WriteLine("Amount withdrawn by Bank Manager"); } } }
看得出来我们的代码有一些问题:
- 在这里添加额外层级的员工是很难管理的,其会导致if/else语句的混乱。
- 在监督员级别检查ID的特殊逻辑是某种难以进行单元测试的东西,因为其首先需要传递一些其他的检查。
- 当仅有的定义逻辑是当时提取的金额,我们可以在未来添加额外的检查(比如,VIP客户被如此标记并总是被监督员处理)。这个逻辑将会变得很难维护并很容易失控。
职责链模式代码
让我们重写下代码。我们创建了employee对象,其可以处理判断他们本身能否处理这个请求的逻辑。在那之上,让我们给他们一个Line Manager,这样他们便知道在必要的时候他们可以将请求往上传递。
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("Amount withdrawn by Teller"); } } 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("Account holder does not have ID on record."); } Console.WriteLine("Amount withdrawn by Supervisor"); } } class BankManager : IBankEmployee { public IBankEmployee LineManager { get; set; } public void HandleWithdrawRequest(BankAccount account, decimal amount) { Console.WriteLine("Amount withdrawn by Bank Manager"); } } We can then create the “chain” by creating the employees required along with their managers. Almost like creating an Org Chart. var bankManager = new BankManager(); var bankSupervisor = new Supervisor { LineManager = bankManager }; var frontLineStaff = new Teller { LineManager = bankSupervisor }; We can then completely transform the BankAccount class Withdraw method to instead be handled by our front line staff member (The Teller). class BankAccount { public bool idOnRecord { get; set; } public void WithdrawMoney(IBankEmployee frontLineStaff, decimal amount) { frontLineStaff.HandleWithdrawRequest(this, amount); } }
那么我们便可以通过创建所需的员工及他们的Line Manger 来创建这个链。就像我们在创建一个组织结构图:
var bankManager = new BankManager(); var bankSupervisor = new Supervisor { LineManager = bankManager }; var frontLineStaff = new Teller { LineManager = bankSupervisor };
这样我们可以完全将BankAccount类WithDraw方法被替代为由我们的前线员工来处理(银行出纳):
class BankAccount { public bool idOnRecord { get; set; } public void WithdrawMoney(IBankEmployee frontLineStaff, decimal amount) { frontLineStaff.HandleWithdrawRequest(this, amount); } }
- 现在,当我们做了一个取款申请,总是由出。纳首先处理,如果其不能处理,其会将这个请求传递给它的Line Manager,那可能是任何人。可以看出来,这个模式的优雅之处是:后续的处理节点没必要知道为什么事情被传递给自己。一个监督员不需要知道满足什么样的条件出纳会将请求传递到上面。
- 出纳没有必要知道在它之后的整个链条。他只会将请求传递给监督员,请求便会在哪儿被处理(或者,如果需要的话,进一步传递)。
- 整个组织架构图可以通过引入新的员工类型被改变。举个例子,如果我创建一个出纳主管,其会处理10K~50K的金额请求然后将其传递给监督员。出纳对象将会保持不变,并且我只会将出纳对象的Line Manage改变为“出纳 主管”作为代替。
- 任何我们写的单元测试都会集中于一个单独的员工。举个例子,当我们测试一个监督员时,我们没有必要去测试一个出纳的逻辑。
扩展我们的示例
我认为如上的示例是一个优秀的方式来演示职责链模式,通常你会发现人们使用一个叫做"SetNext"的方法。通常我认为这在C#中极不寻常,因为我们有属性Getters 和 Setters。使用“SetVariableName” 方法通常是来自于使用C++的日子,其是封装变量的更推荐的方式。
但是在那之前,另一个示例同样典型的使用了抽象类来尝试以及限制请求是如何传递的。我们如上示例的问题在于有许多重复的代码来传递请求到下一个处理逻辑。让我们对其进行一点清理。
有许多的代码裸漏在我的面前。我们想做的第一件事是创建一个抽象类,其允许我们以一个标准化的方式来处理取款请求。它应该检查条件,如果通过,执行取款操作,如果没有通过,他需要将取款请求传递给他的Line Manage。它看起来像是这样:
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); }
接下来我们需要更改我们的emloyee类使其继承于这个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("Amount withdrawn by Teller"); } } 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("Account holder does not have ID on record."); } Console.WriteLine("Amount withdrawn by Supervisor"); } } class BankManager : BankEmployee, IBankEmployee { protected override bool CanHandleRequest(BankAccount account, decimal amount) { return true; } protected override void Withdraw(BankAccount account, decimal amount) { Console.WriteLine("Amount withdrawn by Bank Manager"); } }
注意在所有的情形中,来自于抽象类的public方法 “HandleWithdrawRequest”都会被调用。然后其调用子类的“CanHandleRequest”方法,其包含了我们关于此员工是否合适的判断逻辑,然后其调用其本都的“Withdraw”方法。否则会尝试另一个员工。
我们只需要改变我们如何创建员工链,就如同这样:
var bankManager = new BankManager(); var bankSupervisor = new Supervisor(); bankSupervisor.SetLineManager(bankManager); var frontLineStaff = new Teller(); frontLineStaff.SetLineManager(bankSupervisor);
再一次,我宁愿不使用"SetX"方法,但是其是许许多多的示例使用的东西因此我认为我应该包括它。
也有些示例会将一个员工是否能够处理请求的判断逻辑放在实际的抽象类中。我个人倾向于不这么做,因为这意味着我们的处理逻辑不得不具有非常相似的逻辑。例如,现在所有人都在检查要提取的金额,但是如果我们有一个特定的处理程序在寻找特定的东西(比如VIP标志?),将该逻辑添加到某些处理程序而不是其他处理程序的抽象类中只会让我们回到If/Else地狱。
什么时候使用“职责链模式”?
这个模式的最佳使用情形是你有一个非常具有逻辑性的处理器“链表”,其会在每次按顺序运行。我注意到链表的分叉会是这个模式的一个变量。而且很快会变得极其复杂于处理。由于这个原因,当我为真实世界的“命令链”场景建模时,我通常会使用这种模式,这是我用银行作为示例的所有全部原因,因为它是真实世界的可以用代码来模型化的“职责链”。