职责链形式

亦称: 职责链形式、指令链、CoR、Chain of Command、Chain of Responsibility

意图

职责链形式是一种行为规划形式, 答应你将恳求沿着处理者链进行发送。 收到恳求后, 每个处理者均可对恳求进行处理, 或将其传递给链上的下个处理者。

2023 跟我一起学「设计模式」: 责任链模式

问题

假如你正在开发一个在线订货体系。 你期望对体系访问进行约束, 只答应认证用户创立订单。 此外, 具有管理权限的用户也具有一切订单的完全访问权限。

简略规划后, 你会意识到这些查看有必要顺次进行。 只需接收到包括用户凭证的恳求, 运用程序就可尝试对进入体系的用户进行认证。 但假如因为用户凭证不正确而导致认证失利, 那就没有必要进行后续查看了。

2023 跟我一起学「设计模式」: 责任链模式

恳求有必要经过一系列查看后才干由订货体系来处理。

在接下来的几个月里, 你完成了后续的几个查看过程。

  • 一位搭档以为直接将原始数据传递给订货体系存在安全隐患。 因此你新增了额定的验证过程来整理恳求中的数据。
  • 过了一段时间, 有人注意到体系无法抵挡暴力密码破解办法的攻击。 为了防备这种状况, 你立刻添加了一个查看过程来过滤来自同一 IP 地址的重复过错恳求。
  • 又有人提议你能够对包括相同数据的重复恳求返回缓存中的成果, 然后进步体系响应速度。 因此, 你新增了一个查看过程, 确保只要没有满意条件的缓存成果时恳求才干经过并被发送给体系。

2023 跟我一起学「设计模式」: 责任链模式

代码变得越来越多, 也越来越紊乱。

查看代码本来就现已紊乱不胜, 而每次新增功能都会使其愈加臃肿。 修正某个查看过程有时会影响其他的查看过程。 最糟糕的是, 当你期望复用这些查看过程来保护其他体系组件时, 你只能复制部分代码, 因为这些组件只需部分而非全部的查看过程。

体系会变得让人非常费解, 并且其保护本钱也会激增。 你在艰难地和这些代码同处一段时间后, 有一天总算决议对整个体系进行重构。

解决方案

与许多其他行为规划形式相同, 职责链会将特定行为转换为被称作处理者的独立目标。 在上述示例中, 每个查看过程都可被抽取为仅有单个办法的类, 并履行查看操作。 恳求及其数据则会被作为参数传递给该办法。

形式主张你将这些处理者连成一条链。 链上的每个处理者都有一个成员变量来保存关于下一处理者的引证。 除了处理恳求外, 处理者还担任沿着链传递恳求。 恳求会在链上移动, 直至一切处理者都有时机对其进行处理。

最重要的是: 处理者能够决议不再沿着链传递恳求, 这可高效地撤销一切后续处理过程。

在我们的订货体系示例中, 处理者会在进行恳求处理工作后决议是否继续沿着链传递恳求。 假如恳求中包括正确的数据, 一切处理者都将履行自己的首要行为, 无论该行为是身份验证还是数据缓存。

2023 跟我一起学「设计模式」: 责任链模式

处理者顺次摆放, 组成一条链。

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

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

2023 跟我一起学「设计模式」: 责任链模式

目标树的枝干能够组成一条链。

一切处理者类均完成同一接口是关键所在。 每个详细处理者仅关怀下一个包括 execute履行办法的处理者。 这样一来, 你就能够在运行时运用不同的处理者来创立链, 而无需将相关代码与处理者的详细类进行耦合。

实在世界类比

2023 跟我一起学「设计模式」: 责任链模式

给技术支持打电话时你或许得应对多名接听人员。

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

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

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

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

职责链形式结构

2023 跟我一起学「设计模式」: 责任链模式

  1. 处理者 (Handler) 声明晰一切详细处理者的通用接口。 该接口一般仅包括单个办法用于恳求处理, 但有时其还会包括一个设置链上下个处理者的办法。

  2. 根底处理者 (Base Handler) 是一个可选的类, 你能够将一切处理者共用的样本代码放置在其间。

    一般状况下, 该类中定义了一个保存关于下个处理者引证的成员变量。 客户端可经过将处理者传递给上个处理者的结构函数或设定办法来创立链。 该类还能够完成默许的处理行为: 确认下个处理者存在后再将恳求传递给它。

  3. 详细处理者 (Concrete Handlers) 包括处理恳求的实践代码。 每个处理者接收到恳求后, 都有必要决议是否进行处理, 以及是否沿着链传递恳求。

    处理者一般是独立且不可变的, 需求经过结构函数一次性地取得一切必要地数据。

  4. 客户端 (Client) 可根据程序逻辑一次性或许动态地生成链。 值得注意的是, 恳求可发送给链上的恣意一个处理者, 而非有必要是第一个处理者。

伪代码

在本例中, 职责链形式担任为活动的 GUI 元素显现上下文协助信息。

2023 跟我一起学「设计模式」: 责任链模式

GUI 类运用组合形式生成。 每个元素都链接到自己的容器元素。 你可随时构建从当时元素开端的、 遍历其一切容器的元素链。

运用程序的 GUI 一般为目标树结构。 例如, 担任烘托程序主窗口的 对话框类就是目标树的根节点。 对话框包括 面板 , 而面板或许包括其他面板, 或是 按钮文本框等基层元素。

只需给一个简略的组件指定协助文本, 它就可显现简短的上下文提示。 但更杂乱的组件可自定义上下文协助文本的显现办法, 例如显现手册摘抄内容或在浏览器中翻开一个网页。

2023 跟我一起学「设计模式」: 责任链模式

协助恳求如何在 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. 因为链的动态性, 客户端需求准备好处理以下状况:

    • 链中或许只要单个链接。
    • 部分恳求或许无法抵达链尾。
    • 其他恳求或许直到链尾都未被处理。

职责链形式优缺点

  • 你能够操控恳求处理的次序。

  • 单一职责准则。 你可对建议操作和履行操作的类进行解耦。

  • 开闭准则。 你能够在不更改现有代码的状况下在程序中新增处理者。

  • 部分恳求或许未被处理。

代码示例

  • Golang 职责链形式解说和代码示例 – 掘金 ())