设计模式——责任链模式(行为模式)

引言

责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

问题

假如你正在开发一个在线订购系统。 你希望对系统访问进行限制, 只允许认证用户创建订单。 此外, 拥有管理权限的用户也拥有所有订单的完全访问权限。

简单规划后, 你会意识到这些检查必须依次进行。 只要接收到包含用户凭据的请求, 应用程序就可尝试对进入系统的用户进行认证。 但如果由于用户凭据不正确而导致认证失败, 那就没有必要进行后续检查了。

在接下来的几个月里, 你实现了后续的几个检查步骤。

  • 一位同事认为直接将原始数据传递给订购系统存在安全隐患。 因此你新增了额外的验证步骤来清理请求中的数据。

  • 过了一段时间, 有人注意到系统无法抵御暴力密码破解方式的攻击。 为了防范这种情况, 你立刻添加了一个检查步骤来过滤来自同一 IP 地址的重复错误请求。

  • 又有人提议你可以对包含同样数据的重复请求返回缓存中的结果, 从而提高系统响应速度。 因此, 你新增了一个检查步骤, 确保只有没有满足条件的缓存结果时请求才能通过并被发送给系统。

检查代码本来就已经混乱不堪, 而每次新增功能都会使其更加臃肿。 修改某个检查步骤有时会影响其他的检查步骤。 最糟糕的是, 当你希望复用这些检查步骤来保护其他系统组件时, 你只能复制部分代码, 因为这些组件只需部分而非全部的检查步骤。

系统会变得让人非常费解, 而且其维护成本也会激增。 你在艰难地和这些代码共处一段时间后, 有一天终于决定对整个系统进行重构。

解决方案

与许多其他行为设计模式一样, 责任链会将特定行为转换为被称作处理者的独立对象。 在上述示例中, 每个检查步骤都可被抽取为仅有单个方法的类, 并执行检查操作。 请求及其数据则会被作为参数传递给该方法。

模式建议你将这些处理者连成一条链。 链上的每个处理者都有一个成员变量来保存对于下一处理者的引用。 除了处理请求外, 处理者还负责沿着链传递请求。 请求会在链上移动, 直至所有处理者都有机会对其进行处理。

最重要的是: 处理者可以决定不再沿着链传递请求, 这可高效地取消所有后续处理步骤。

在我们的订购系统示例中, 处理者会在进行请求处理工作后决定是否继续沿着链传递请求。 如果请求中包含正确的数据, 所有处理者都将执行自己的主要行为, 无论该行为是身份验证还是数据缓存。

不过还有一种稍微不同的方式 (也是更经典一种), 那就是处理者接收到请求后自行决定是否能够对其进行处理。 如果自己能够处理, 处理者就不再继续传递请求。 因此在这种情况下, 每个请求要么最多有一个处理者对其进行处理, 要么没有任何处理者对其进行处理。 在处理图形用户界面元素栈中的事件时, 这种方式非常常见。

例如, 当用户点击按钮时, 按钮产生的事件将沿着 GUI 元素链进行传递, 最开始是按钮的容器 (如窗体或面板), 直至应用程序主窗口。 链上第一个能处理该事件的元素会对其进行处理。 此外, 该例还有另一个值得我们关注的地方: 它表明我们总能从对象树中抽取出链来。

所有处理者类均实现同一接口是关键所在。 每个具体处理者仅关心下一个包含 execute执行方法的处理者。 这样一来, 你就可以在运行时使用不同的处理者来创建链, 而无需将相关代码与处理者的具体类进行耦合。

真实世界类比 

最近, 你刚为自己的电脑购买并安装了一个新的硬件设备。 身为一名极客, 你显然在电脑上安装了多个操作系统, 所以你会试着启动所有操作系统来确认其是否支持新的硬件设备。 Windows 检测到了该硬件设备并对其进行了自动启用。 但是你喜爱的 Linux 系统并不支持新硬件设备。 抱着最后一点希望, 你决定拨打包装盒上的技术支持电话。

首先你会听到自动回复器的机器合成语音, 它提供了针对各种问题的九个常用解决方案, 但其中没有一个与你遇到的问题相关。 过了一会儿, 机器人将你转接到人工接听人员处。

这位接听人员同样无法提供任何具体的解决方案。 他不断地引用手册中冗长的内容, 并不会仔细聆听你的回应。 在第 10 次听到 “你是否关闭计算机后重新启动呢?” 这句话后, 你要求与一位真正的工程师通话。

最后, 接听人员将你的电话转接给了工程师, 他或许正缩在某幢办公大楼的阴暗地下室中, 坐在他所深爱的服务器机房里, 焦躁不安地期待着同一名真人交流。 工程师告诉了你新硬件设备驱动程序的下载网址, 以及如何在 Linux 系统上进行安装。 问题终于解决了! 你挂断了电话, 满心欢喜。

责任链模式结构

伪代码

在本例中, 责任链模式负责为活动的 GUI 元素显示上下文帮助信息。

应用程序的 GUI 通常为对象树结构。 例如, 负责渲染程序主窗口的 对话框类就是对象树的根节点。 对话框包含 面板 , 而面板可能包含其他面板, 或是 按钮和 文本框等下层元素。

只要给一个简单的组件指定帮助文本, 它就可显示简短的上下文提示。 但更复杂的组件可自定义上下文帮助文本的显示方式, 例如显示手册摘录内容或在浏览器中打开一个网页。

当用户将鼠标指针移动到某个元素并按下 F1键时, 程序检测到指针下的组件并对其发送帮助请求。 该请求不断向上传递到该元素所有的容器, 直至某个元素能够显示帮助信息。

// 处理者接口声明了一个创建处理者链的方法。还声明了一个执行请求的方法。
interface ComponentWithContextualHelp is
    method showHelp()


// 简单组件的基础类。
abstract class Component implements ComponentWithContextualHelp is
    field tooltipText: string

    // 组件容器在处理者链中作为“下一个”链接。
    protected field container: Container

    // 如果组件设定了帮助文字,那它将会显示提示信息。如果组件没有帮助文字
    // 且其容器存在,那它会将调用传递给容器。
    method showHelp() is
        if (tooltipText != null)
            // 显示提示信息。
        else
            container.showHelp()


// 容器可以将简单组件和其他容器作为其子项目。链关系将在这里建立。该类将从
// 其父类处继承 showHelp(显示帮助)的行为。
abstract class Container extends Component is
    protected field children: array of Component

    method add(child) is
        children.add(child)
        child.container = this


// 原始组件应该能够使用帮助操作的默认实现……
class Button extends Component is
    // ……

// 但复杂组件可能会对默认实现进行重写。如果无法以新的方式来提供帮助文字,
// 那组件总是还能调用基础实现的(参见 Component 类)。
class Panel extends Container is
    field modalHelpText: string

    method showHelp() is
        if (modalHelpText != null)
            // 显示包含帮助文字的模态窗口。
        else
            super.showHelp()

// ……同上……
class Dialog extends Container is
    field wikiPageURL: string

    method showHelp() is
        if (wikiPageURL != null)
            // 打开百科帮助页面。
        else
            super.showHelp()


// 客户端代码。
class Application is
    // 每个程序都能以不同方式对链进行配置。
    method createUI() is
        dialog = new Dialog("预算报告")
        dialog.wikiPageURL = "http://……"
        panel = new Panel(0, 0, 400, 800)
        panel.modalHelpText = "本面板用于……"
        ok = new Button(250, 760, 50, 20, "确认")
        ok.tooltipText = "这是一个确认按钮……"
        cancel = new Button(320, 760, 50, 20, "取消")
        // ……
        panel.add(ok)
        panel.add(cancel)
        dialog.add(panel)

    // 想象这里会发生什么。
    method onF1KeyPress() is
        component = this.getComponentAtMouseCoords()
        component.showHelp()

责任链模式适合应用场景

  •  当程序需要使用不同方式处理不同种类请求, 而且请求类型和顺序预先未知时, 可以使用责任链模式。
  •  该模式能将多个处理者连接成一条链。 接收到请求后, 它会 “询问” 每个处理者是否能够对其进行处理。 这样所有处理者都有机会来处理请求。
  •  当必须按顺序执行多个处理者时, 可以使用该模式。
  •  无论你以何种顺序将处理者连接成一条链, 所有请求都会严格按照顺序通过链上的处理者。
  •  如果所需处理者及其顺序必须在运行时进行改变, 可以使用责任链模式。
  •  如果在处理者类中有对引用成员变量的设定方法, 你将能动态地插入和移除处理者, 或者改变其顺序。

实现方式

  1. 声明处理者接口并描述请求处理方法的签名。

    确定客户端如何将请求数据传递给方法。 最灵活的方式是将请求转换为对象, 然后将其以参数的形式传递给处理函数。

  2. 为了在具体处理者中消除重复的样本代码, 你可以根据处理者接口创建抽象处理者基类。

    该类需要有一个成员变量来存储指向链上下个处理者的引用。 你可以将其设置为不可变类。 但如果你打算在运行时对链进行改变, 则需要定义一个设定方法来修改引用成员变量的值。

    为了使用方便, 你还可以实现处理方法的默认行为。 如果还有剩余对象, 该方法会将请求传递给下个对象。 具体处理者还能够通过调用父对象的方法来使用这一行为。

  3. 依次创建具体处理者子类并实现其处理方法。 每个处理者在接收到请求后都必须做出两个决定:

    • 是否自行处理这个请求。
    • 是否将该请求沿着链进行传递。
  4. 客户端可以自行组装链, 或者从其他对象处获得预先组装好的链。 在后一种情况下, 你必须实现工厂类以根据配置或环境设置来创建链。

  5. 客户端可以触发链中的任意处理者, 而不仅仅是第一个。 请求将通过链进行传递, 直至某个处理者拒绝继续传递, 或者请求到达链尾。

  6. 由于链的动态性, 客户端需要准备好处理以下情况:

    • 链中可能只有单个链接。
    • 部分请求可能无法到达链尾。
    • 其他请求可能直到链尾都未被处理。

责任链模式优缺点

  •  你可以控制请求处理的顺序。
  •  单一职责原则。 你可对发起操作和执行操作的类进行解耦。
  •  开闭原则。 你可以在不更改现有代码的情况下在程序中新增处理者。
  •  部分请求可能未被处理。

与其他模式的关系

  • 责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:

    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
    • 命令在发送者和请求者之间建立单向连接。
    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
    • 观察者允许接收者动态地订阅或取消接收请求。
  • 责任链通常和组合模式结合使用。 在这种情况下, 叶组件接收到请求后, 可以将请求沿包含全体父组件的链一直传递至对象树的底部。

  • 责任链的管理者可使用命令模式实现。 在这种情况下, 你可以对由请求代表的同一个上下文对象执行许多不同的操作。

    还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 你可以对由一系列不同上下文连接而成的链执行相同的操作。

  • 责任链和装饰模式的类结构非常相似。 两者都依赖递归组合将需要执行的操作传递给一系列对象。 但是, 两者有几点重要的不同之处。

    责任链的管理者可以相互独立地执行一切操作, 还可以随时停止传递请求。 另一方面, 各种装饰可以在遵循基本接口的情况下扩展对象的行为。 此外, 装饰无法中断请求的传递。  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/254248.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【网络安全】-Linux操作系统—CentOS安装、配置

文章目录 准备工作下载CentOS创建启动盘确保硬件兼容 安装CentOS启动安装程序分区硬盘网络和主机名设置开始安装完成安装 初次登录和配置更新系统安装额外的软件仓库安装网络工具配置防火墙设置SELinux安装文本编辑器配置SSH服务 总结 CentOS是一个基于Red Hat Enterprise Linu…

基于html5的演唱会购票系统的设计与实现论文

基于html5的演唱会购票系统的设计与实现 摘要 随着信息互联网购物的飞速发展,一般企业都去创建属于自己的电商平台以及购物管理系统。本文介绍了基于html5的演唱会购票系统的设计与实现的开发全过程。通过分析企业对于基于html5的演唱会购票系统的设计与实现的需求…

13.仿简道云公式函数实战-逻辑函数-NOT

1. NOT函数 NOT 函数可用于对其参数的逻辑求反,当逻辑为 true 时,返回结果 false;当逻辑为 false 时,返回结果 true。 2. 函数用法 NOT(logical) 3. 函数示例 1)NOT(A),表示如果 A 为 true 时&#xf…

惯性导航基础知识学习----01惯性器件相关

🌈武汉大学惯性导航课程合集是入门惯导的精品课程~ 作为导航路上的鼠鼠我,要开始学习惯性导航了~ 需要达到的要求是大致了解惯导的原理等~ 后期会陆续更新惯导相关的知识和笔记等~ 🐬 本blog为 武汉大学惯性导航课程 的记录~ 感谢团队提供的开…

提升广东省水泥行业效率的MES解决方案

广东省水泥行业作为我国重要的基础建设材料生产领域之一,提高其生产效率和降低能源消耗具有重要意义。而随着技术的发展,我国的MES系统技术发展,通过引入MES系统成为了实现降本增效目标的必要且有效的手段。MES是一种基于计算机技术的管理工具…

argparse --- 命令行选项、参数和子命令解析器详解

一、argparse简介 argparse模块提供了非常方便的命令行参数解析功能,能够大大简化命令行程序的开发。 现在的大型项目中都会运用argparse来管理项目中涉及的参数,在使用命令行时更好地定义模型参数。 argparse定义四个步骤: 导入argparse包…

条款5:了解c++默默编写并调用了哪些函数

如果你不自己声明,编译器会替你声明(编译器版本的)拷贝构造函数、拷贝赋值运算符和析构函数。此外,如果你没有声明任何构造函数,编译器会为你声明一个默认构造函数。 class Empty{};本质上和写成下面这样是一样的: c…

LLM之Agent(六)| 使用AutoGen、LangChian、RAG以及函数调用构建超级对话系统

本文我们将尝试AutoGen集成函数调用功能。函数调用最早出现在Open AI API中,它允许用户调用外部API来增强系统的整体功能和效率。例如,在对话过程中根据需要调用天气API。 函数调用和Agent有各种组合,在这里我们将通过函数调用调用RAG检索增强…

SpringBoot 3.2.0 版本 mysql 依赖下载错误

最近想尝试一下最新的 SpringBoot 项目,于是将自己的开源项目进行了一些升级。 JDK 版本从 JDK8 升级至 JDK17。SpringBoot 版本从 SpringBoot 2.7.3 升级到 SpringBoot 3.2.0 其中 JDK 的升级比较顺利,毕竟 JDK 的旧版本兼容性一直非常好。 但是在升级…

红酒为何会变蓝?这是什么原因?

有的朋友发现红酒酒渍当天没有清洗,第二天会变蓝。于是非常好奇,明明红色的葡萄酒,怎么会变蓝呢? 葡萄酒不但含有酒精,还含有丰富的天然色素花青素。就像我们平时在家煮紫薯粥,加一点小苏打明明紫红色的粥就…

C++计算(a+b)*(c-b)的值 2023年9月c++一级 电子学会中小学生软件编程C++等级考试一级真题答案解析

目录 C计算(ab)*(c-b)的值 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 C计算(ab)*(c-b)的值 2023年9月 C编程等级考试一级编程题 一、题目要求 1、编程实现 给定3个整数a、b、c,计算表达…

爬楼梯算法

计算跳到n阶的跳法总数 package com.zxj.algorithm.动态规划;import lombok.extern.slf4j.Slf4j;import java.util.Arrays;/*** 递归函数 f(n): 计算跳到n阶的跳法总数。* <p>* f(0) 0,* f(1) 1,* f(2) 2,* f(n) f(n-1) f(n-2)*/ Slf4j public class 爬楼梯 {/*** …

【MATLAB】数据拟合第12期-基于高斯核回归的拟合算法

有意向获取代码&#xff0c;请转文末观看代码获取方式~也可转原文链接获取~ 1 基本定义 基于高斯核回归的拟合算法是一种处理回归问题的机器学习方法。以下是该算法的简单介绍&#xff1a; 核心思想&#xff1a;高斯核回归的核心思想是利用高斯核函数对数据点进行非线性映射&a…

SpringBoot已经禁掉了循环依赖!

还在问循环依赖嘛&#xff1f;SpringBoot已经禁掉了循环依赖&#xff01; 首发2023-12-18 11:26yuan人生 如果现在面试时还有人问你循环依赖&#xff0c;你就这样怼他&#xff1a;循环依赖是一种代码质量低下的表现&#xff0c;springboot2.6之后的版本已经默认禁用了。 Spr…

【MySQL】数据库基础入门 安装MySQL

目录 介绍&#xff1a; 安装MySQL: 设置 root 账号密码 2.配置环境变量 2.找到 Path 系统变量, 点击 "编辑" 介绍&#xff1a; MySQL是一个开源的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;它是一种用于管理和存储数据的软件。 安装MySQL: …

Vue3进阶:常用的指令缩写详解,以及代码使用示例

目录 v-bind缩写 v-on缩写 v-if和v-show缩写 v-for缩写 总结 更多关于Vue前端相关技术点&#xff0c;敬请关注公众号&#xff1a;CTO Plus后续的发文&#xff0c;有问题欢迎后台留言交流。 注意&#xff1a;由于排版太费时间&#xff0c;所以还是多多注重技术干货的内容吧…

国产电源芯片SCT2450QSTER,替代TPS54540-Q1,车规级36V 5A输出同步降压DCDC转换器

国产SCT2450Q是一款输出电流高达5A的高功率密度全集成同步降压DCDC转换器。其输入电压范围为3.8V到36V&#xff0c;集成了48mΩ高压侧MOSFET和23mΩ低压侧MOSFET。SCT2450Q采用峰值电流控制模式&#xff0c;可支持具有典型25uA低静态电流的脉冲跳过调制&#xff08;PSM&#xf…

Charles 安装与激活

步骤 1&#xff1a;购买 Charles 许可证 访问 Charles 官方网站&#xff1a;https://www.charlesproxy.com/ 在网站上查找并选择 “Buy” 或类似的选项。 选择适合你需求的许可证类型&#xff0c;填写相关信息并完成购买。 如果不想购买可点击此链接Charles 步骤 2&#xff…

ESP32 连接阿里云 MQTT 报错MQTT Connect err:2

解决方法 跳转到 这个头文件<PubSubClient.h>里 MQTT_MAX_PACKET_SIZE 把这个的大小从原来的256 改为1024 MQTT_KEEPALIVE 把这个大小从原来的15 改为65 修改后再次连接即可成功 如下图&#xff1a;

人工智能在电子商务中的十大最佳用例

“如果你没有躲在山洞里&#xff0c;你可能听说过人工智能&#xff08;AI&#xff09;这个神奇的东西。它就像一个超级智能的计算机大脑&#xff0c;像电子商务中的人工智能一样无处不在。你猜怎么着&#xff1f;它甚至进入了在线购物领域&#xff0c;这真是太棒了。” 人工智…