百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

如何在C#/.NET Core中使用责任链模式

liebian365 2024-11-17 13:18 3 浏览 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地狱中。

什么时候应该使用责任链模式?

这种模式最佳的使用场景是,你的业务上有一个逻辑上的处理链,这个处理链每次必须按照顺序运行。这里请注意,链分叉是这种模式的一个变体, 但是很快处理起来就会非常复杂。因此,当我对现实世界中"命令链"场景建模的时候,我通常会使用这种模式。这就是我以银行为例的原因,因为它就是现实世界中可以用代码建模的"责任链"。

相关推荐

快递查询教程,批量查询物流,一键管理快递

作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...

一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递

对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...

快递查询单号查询,怎么查物流到哪了

输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...

3分钟查询物流,教你一键批量查询全部物流信息

很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...

快递单号查询,一次性查询全部物流信息

现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...

快递查询工具,批量查询多个快递快递单号的物流状态、签收时间

最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...

快递查询软件,自动识别查询快递单号查询方法

当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...

教你怎样查询快递查询单号并保存物流信息

商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...

简单几步骤查询所有快递物流信息

在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...

物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号

最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...

连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息

快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...

快递查询教程,快递单号查询,筛选更新量为1的单号

最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: