如何在C#/.NET Core中使用责任链模式
liebian365 2024-11-17 13:18 15 浏览 0 评论
前言
最近我有一个朋友在研究经典的"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地狱中。
什么时候应该使用责任链模式?
这种模式最佳的使用场景是,你的业务上有一个逻辑上的处理链,这个处理链每次必须按照顺序运行。这里请注意,链分叉是这种模式的一个变体, 但是很快处理起来就会非常复杂。因此,当我对现实世界中"命令链"场景建模的时候,我通常会使用这种模式。这就是我以银行为例的原因,因为它就是现实世界中可以用代码建模的"责任链"。
相关推荐
- 4万多吨豪华游轮遇险 竟是因为这个原因……
-
(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...
- “菜鸟黑客”必用兵器之“渗透测试篇二”
-
"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...
- 科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白
-
作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...
- 麦子陪你做作业(二):KEGG通路数据库的正确打开姿势
-
作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...
- 知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势
-
智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...
- 每日新闻播报(September 14)_每日新闻播报英文
-
AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...
- 香港新巴城巴开放实时到站数据 供科技界研发使用
-
中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...
- 5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper
-
本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...
- Qt动画效果展示_qt显示图片
-
今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...
- 如何从0到1设计实现一门自己的脚本语言
-
作者:dong...
- 三年级语文上册 仿写句子 需要的直接下载打印吧
-
描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...
- C++|那些一看就很简洁、优雅、经典的小代码段
-
目录0等概率随机洗牌:1大小写转换2字符串复制...
- 二年级上册语文必考句子仿写,家长打印,孩子照着练
-
二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)