如何在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地狱中。
什么时候应该使用责任链模式?
这种模式最佳的使用场景是,你的业务上有一个逻辑上的处理链,这个处理链每次必须按照顺序运行。这里请注意,链分叉是这种模式的一个变体, 但是很快处理起来就会非常复杂。因此,当我对现实世界中“命令链”场景建模的时候,我通常会使用这种模式。这就是我以银行为例的原因,因为它就是现实世界中可以用代码建模的“责任链”。
如何在C#/.NET Core中使用责任链模式的更多相关文章
- Python使用设计模式中的责任链模式与迭代器模式的示例
Python使用设计模式中的责任链模式与迭代器模式的示例 这篇文章主要介绍了Python使用设计模式中的责任链模式与迭代器模式的示例,责任链模式与迭代器模式都可以被看作为行为型的设计模式,需要的朋友可 ...
- Netty中的责任链模式
适用场景: 对于一个请求来说,如果有个对象都有机会处理它,而且不明确到底是哪个对象会处理请求时,我们可以考虑使用责任链模式实现它,让请求从链的头部往后移动,直到链上的一个节点成功处理了它为止 优点: ...
- JAVA中的责任链模式(CH01)
责任链模式的关键在于每一个任务处理者都必须持有下一个任务处理者的作用 纯的责任链:纯的责任链是只能也必须只有一个任务处理者去处理这个任务, 不会出现没有处理者处理的情况,也不会出现有多个处 ...
- JAVA中的责任链模式(CH02)
对责任链CH01做出优化,解决耦合度太高问题 记得上一篇我们使用的是抽象类,然后用子类去继承的方法实现等级的桥接,从而发现了耦合度太高. 为了解决这个问题. 我们本次使用接口进行抽象,然后使用到一个” ...
- Spring是如何使用责任链模式的?
关于责任链模式,其有两种形式,一种是通过外部调用的方式对链的各个节点调用进行控制,从而进行链的各个节点之间的切换. 另一种是链的每个节点自由控制是否继续往下传递链的进度,这种比较典型的使用方式就是Ne ...
- 责任链模式/chain of responsibility/行为型模式
职责链模式 chain of responsibility 意图 使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系.将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处 ...
- 编写计算器程序学习JS责任链模式
设计模式中的责任链模式能够很好的处理程序过程的逻辑判断,提高程序可读性. 责任链模式的核心在于责任链上的元素判断能够处理该数据,不能处理的话直接交给它的后继者. 计算器的基本样式: 通过div+css ...
- ASP.NET MVC 学习笔记-2.Razor语法 ASP.NET MVC 学习笔记-1.ASP.NET MVC 基础 反射的具体应用 策略模式的具体应用 责任链模式的具体应用 ServiceStack.Redis订阅发布服务的调用 C#读取XML文件的基类实现
ASP.NET MVC 学习笔记-2.Razor语法 1. 表达式 表达式必须跟在“@”符号之后, 2. 代码块 代码块必须位于“@{}”中,并且每行代码必须以“: ...
- 责任链模式的具体应用 ServiceStack.Redis订阅发布服务的调用
责任链模式的具体应用 1.业务场景 生产车间中使用的条码扫描,往往一把扫描枪需要扫描不同的条码来处理不同的业务逻辑,比如,扫描投入料工位条码.扫描投入料条码.扫描产出工装条码等,每种类型的条码位数 ...
随机推荐
- 彻底卸载----LoadRunner
保证所有LoadRunner的相关进程(包括Controller.VuGen.Analysis和Agent Process)全部关闭: 备份好LoadRunner安装目录下测试脚本,这些脚本一般存放在 ...
- 教你爬取腾讯课堂、网易云课堂、mooc等所有课程信息
本文的所有代码都在GitHub上托管,想要代码的同学请点击这里
- log4j入门(转) --- 很详细 也很简单容易懂
log4j入门(转) Log4j实在是很熟悉,几乎所有的Java项目都用它啊.但是我确一直没有搞明白.终于有一天我受不了了,定下心去看了一把文档,才两个小时,我终于搞明白了.一般情况下Log4j总是和 ...
- [科普向] Roguelike游戏到底是什么?
简单的说 Roguelike 是 RPG(角色扮演游戏)的一个分支,也是最重要的一个分支.这个名字源于 1980 年发布的著名电子游戏<Rogue>.按字面上理解,Roguelike 就是 ...
- 2019-07-31【机器学习】无监督学习之降维NMF算法 (人脸特征提取)
代码 from numpy.random import RandomState #加载RandomState用于创建随机种子 import matplotlib.pyplot as plt from ...
- 解决Jquery中click里面包含click事件,出现重复执行的问题
出现问题的代码: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.o ...
- Daily Scrum 1/4/2015
Process: After New year's Day, we start our project in plan. Zhanyang: Add some useful UI in the IOS ...
- Hash记录字符串
Hash记录字符串模板: mod常常取1e9+7,base常常取299,,127等等等....有的题目会卡Hash,因为可能会有两个不同的Hash但却有相通的Hash值...这个时候可以用双Hash来 ...
- Cocos2d-x在win7下的android交叉编译环境
cocos2d-x在win7下的Android交叉编译环境 2014年4月14日 cocos2d-x环境配置 前面把Visual Studio+Python开发环境配好了,但还没有讲如何在Androi ...
- 前端学习笔记-JavaScript
js引入方式: 1.嵌入js的方式:直接在页内的script标签内书写js功能代码. <script type="text/javascript">alert('hel ...