编程

设计模式之责任链(Chain of Responsibility)模式

195 2024-02-01 23:02:00

又名: CoR, Chain of Command

意图

责任链(Chain of Responsibility )模式是一种行为设计模式,允许沿着处理程序链传递请求。收到请求后,每个处理程序决定处理该请求或将其传递给链中的下一个处理程序。

问题描述

假设你正在开发一个在线订单系统。你希望限制对系统的访问,以便只有经过身份验证的用户才能创建订单。此外,具有管理权限的用户必须具有对所有订单的完全访问权限。

经过一点计划,你意识到这些检查必须按顺序执行。每当收到包含用户凭据的请求时,应用程序都可以尝试向系统验证用户。但是,如果这些凭据不正确并且身份验证失败,则没有理由继续进行任何其他检查。

在接下来的几个月里,你又实施了几个这样的顺序检查。

你的一位同事表示,将原始数据直接传递给订单系统是不安全的。因此,你添加了一个额外的验证步骤来净化请求中的数据。

后来,有人注意到该系统很容易被暴力破解密码。为了否定这一点,你立即添加了一个检查,用于过滤来自同一 IP 地址的重复失败请求。

还有人建议,你可以通过返回包含相同数据的重复请求的缓存结果来加快系统速度。因此,你添加了另一个检查,只有没有合适的缓存响应时,才允许将请求传递到系统。

检查的代码看起来已经一团糟,随着每次添加新功能,它变得越来越臃肿。更改一个检查有时会影响其他检查。最糟糕的是,当试图重新使用检查来保护系统的其他组件时,你不得不复制一些代码,因为这些组件需要一些检查,但不是所有的检查。

这个系统变得很难理解,维护成本也很高。在代码上挣扎了一段时间后,直到有一天你决定重构整个代码。

方案

与许多其他行为设计模式一样,责任链( Chain of Responsibility)依赖于将特定行为转换为独立对象的 handler。在我们的例子中,每个检查都应该用一个执行检查的方法将其提取到自己的类中。请求及其数据将作为参数传递给该方法。

该模式建议您将这些处理程序(handler)链接到一个链中。每个链接的 handler 都有一个字段,用于存储对链中下一个 handler 的引用。除了处理请求之外,handler 还会沿着链进一步传递请求。请求沿着链传播,直到所有 handler 有机会处理它。

最棒的是:handler 可以决定不将请求进一步传递到链的下游,并有效地停止任何进一步的处理。

在我们的订单系统示例中,handler 执行处理,然后决定是否将请求进一步传递到链的下游。假设请求包含正确的数据,则所有 handler 都可以执行其主要行为,无论是身份验证检查还是缓存。

不过,有一种稍微不同的方法(而且更规范),在接收到请求后,handler 决定是否可以处理它。如果可以,它就不再传递请求。因此,要么只有一个 handler 来处理请求,要么根本没有。这种方法在处理图形用户界面中元素堆栈中的事件时非常常见。

例如,当用户单击按钮时,事件通过 GUI 元素链传播,该元素链从按钮开始,沿着其容器(如表单或面板)传播,最后到达主应用程序窗口。事件由链中能够处理它的第一个元素进行处理。这个例子也值得注意,因为它表明了链总是可以从对象树中提取的。

至关重要的是,所有处理程序(handler)类都实现相同的接口。每个具体的处理程序应该只关心下面一个具有 execute 方法的处理程序。通过这种方式,你可以在运行时使用各种处理程序来组成链,而无需将代码耦合到它们的具体类。

真实世界类比

你刚刚在电脑上买了一个新的硬件并安装了它。由于你是一个极客,这台电脑安装了几个操作系统。您尝试引导所有这些应用程序,以查看硬件是否受支持。Windows 自动检测并启用硬件。然而,Linux 拒绝使用新硬件。带着一丝希望,你决定拨打盒子上写的技术支持电话号码。

首先听到的是自动应答器的机器人声音。它为各种问题提出了九种流行的解决方案,但都与你的案例无关。过一段时间,机器人会将你连接到一个实时操作员。

唉,接线员也不能提出任何具体的建议。他不断引用手册中的冗长摘录,拒绝听取你的意见。在第 10 次听到“你试过再次关闭和打开电脑吗?”这句话后,你要求连接到合适的工程师。

最终,接线员把你的电话传给了其中一位工程师,他坐在某栋办公楼黑暗地下室里孤独的服务器室里,可能渴望与人实时聊天好几个小时了。工程师告诉在哪里为新硬件下载合适的驱动程序,以及如何在 Linux 上安装它们。最后,解决方案!你结束了通话,充满了喜悦。

结构

Handler 声明了接口,所有具体的 handler 共用该handler。它通常只包含一个处理请求的方法,不过,有时也会有另一个在链上设置下一个 handler 的方法。

Base Handler 是一个可选的类,你可以将所有 handler 共用的模板代码放到其中。

通常,该类定义了一个字段存储下一个 handler 的引用。客户端可以传入一个 handler 给前一个 handler 的构造函数或 setter 创建一个链条。该类同时实现了默认处理行为:它在检查完下一个 handler 是否存在后,将执行传给它。

Concrete Handler 包含处理请求的实际代码。一旦收到请求,每个 handler 必须确定是否对其进行处理以及是否顺着链条传递。

Handler 通常是自包含的和不可变的,通过构造函数只接受一次所有必要的数据。

客户端(Client)可以只组合一次链,也可以根据应用的逻辑动态地组合链。请注意,请求可以发送到链中的任何 handler——它不必是第一个 handler。

伪代码

本例中,责任链(Chain of Responsibility)模式负责为激活的 GUI 元素展示上下文帮助信息。

应用 的 GUI 通常被构造为对象树。例如,渲染应用主窗口的 Dialog 类将是对象树的根。该窗口包含面板 Panel,面板可能包含其他面板或简单的低级元素,如按钮 Button 和文本字段 TextField

一个简单的组件可以显示简短的上下文工具提示,只要该组件指定了一些帮助文本。但更复杂的组件定义了自己显示上下文帮助的方式,例如显示手册摘录或在浏览器中打开页面。

 

Structure of the Chain of Responsibility example
That’s how a help request traverses GUI objects.

当用户将鼠标光标指向某个元素并按下 F1 键时,应用会检测到指针下的组件并向其发送帮助请求。请求在元素的所有容器中冒泡,直到到达能够显示帮助信息的元素。

// The handler interface declares a method for executing a
// request.
interface ComponentWithContextualHelp is
    method showHelp()
// The base class for simple components.
abstract class Component implements ComponentWithContextualHelp is
    field tooltipText: string
    // The component's container acts as the next link in the
    // chain of handlers.
    protected field container: Container
    // The component shows a tooltip if there's help text
    // assigned to it. Otherwise it forwards the call to the
    // container, if it exists.
    method showHelp() is
        if (tooltipText != null)
            // Show tooltip.
        else
            container.showHelp()
// Containers can contain both simple components and other
// containers as children. The chain relationships are
// established here. The class inherits showHelp behavior from
// its parent.
abstract class Container extends Component is
    protected field children: array of Component
    method add(child) is
        children.add(child)
        child.container = this
// Primitive components may be fine with default help
// implementation...
class Button extends Component is
    // ...
// But complex components may override the default
// implementation. If the help text can't be provided in a new
// way, the component can always call the base implementation
// (see Component class).
class Panel extends Container is
    field modalHelpText: string
    method showHelp() is
        if (modalHelpText != null)
            // Show a modal window with the help text.
        else
            super.showHelp()
// ...same as above...
class Dialog extends Container is
    field wikiPageURL: string
    method showHelp() is
        if (wikiPageURL != null)
            // Open the wiki help page.
        else
            super.showHelp()
// Client code.
class Application is
    // Every application configures the chain differently.
    method createUI() is
        dialog = new Dialog("Budget Reports")
        dialog.wikiPageURL = "http://..."
        panel = new Panel(0, 0, 400, 800)
        panel.modalHelpText = "This panel does..."
        ok = new Button(250, 760, 50, 20, "OK")
        ok.tooltipText = "This is an OK button that..."
        cancel = new Button(320, 760, 50, 20, "Cancel")
        // ...
        panel.add(ok)
        panel.add(cancel)
        dialog.add(panel)
    // Imagine what happens here.
    method onF1KeyPress() is
        component = this.getComponentAtMouseCoords()
        component.showHelp()

适用场景

当程序期望以各种方式处理不同类型的请求,但请求的确切类型及其顺序事先未知时,请使用责任链模式。

该模式允许将多个处理程序(handler)链接到一个链中,并在收到请求时“询问”每个处理程序是否可以处理它。这样,所有处理程序都有机会处理请求。

当必须按特定顺序执行多个处理程序(handler)时,请使用该模式。

由于可以按任何顺序链接链中的处理程序(hindler),因此所有请求都将完全按照计划通过链处理。

当处理程序(handler)集及其顺序应该在运行时更改时,请使用CoR模式。

如果在处理程序(handler)类中为引用字段提供 setter,则可以动态地插入、移除或重新排序处理程序。

如何实现

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

确定客户端如何将请求数据传送给该方法。最灵活的方式是经请求信息转换成对象,并将其作为参数传递给处理方法。

要消除在具体 handler 中重复的样板代码,可能需要创建一个从处理程序(handler)接口派生的抽象基本处理程序(handler)类。

这个类应该有一个字段,用于存储对链中下一个处理程序的引用。考虑使类不可变。但是,如果计划在运行时修改链,则需要定义一个用于更改引用字段值的 setter。

还可以为处理方法实现方便的默认行为,即将请求转发到下一个对象,除非没有剩余的对象。具体处理程序(handler)将能够通过调用父类方法来使用此行为。

一个接一个地创建具体的处理程序(handler)子类并实现它们的处理方法。每个处理程序(handler)在接收请求时都应该做出两个决定:

  • 是否处理请求。
  • 是否沿着链传递请求。

客户端可以自己组装链,也可以从其他对象接收预制链。在后一种情况下,必须根据配置或环境设置实现一些工厂类来构建链。

客户端可以触发链中的任何处理程序(handler),而不仅仅是第一个处理程序(handler)。请求将沿着链传递,直到某个处理程序(handler)拒绝进一步传递它,或者直到它到达链的末端。

由于链的动态性质,客户端应准备好处理以下场景:

  • 链条可能只有一个链。
  • 有些请求可能无法到达链的末端。
  • 其他人可能未经处理就到达了链条的末端。

优缺点

  • ✔️你可以控制请求处理顺序。
  • ✔️单一职责原则。你可以将调用操作的类与执行操作的类解耦。
  • ✔️开闭原则。你可以在不破坏现有客户端代码的情况下将新的 handler 引入到应用中。
  • ❌有些请求最终可能无法处理。

与其他模式的关系

责任链模式(Chain of Responsibility)、 命令模式(Command)中介者模式(Mediator)以及观察者模式(Observer)解决连接请求的发送者和接收者的各种方式: 

  • 责任链(Chain of Responsibility) 沿着潜在接收者的动态链顺序传递请求,直到其中一个接收者处理它。
  • 命令(Command)模式在发送者和接收者之间建立了单向连接。
  • 中介者(Mediator)消除了发送者和接收者之间的直接连接,迫使它们通过中介者对象间接通信。
  • 观察者(Observer)允许接收者动态订阅及取消订阅接受请求。

责任链(Chain of Responsibility)经常与组合(Composite)一起使用。在这种情况下,当叶组件收到请求时,它可能会通过所有父组件的链向下传递到对象树的根。

责任链(Chain of Responsibility)中的 handler 可以以命令(Command)的方式实现。这种情况下,你可以对由请求表示的同一上下文对象执行许多不同的操作。

不过还有另一种方式,即请求本身即是命令(Command)对象。这种情况下,你可以在链接到链中的一系列不同上下文中执行相同的操作。

责任链(Chain of Responsibility)装饰器(Decorator)结构非常相似。这两种模式都依赖于递归组合来通过一系列对象传递执行。然而,有几个关键的区别。

CoR 处理程序(handler)可以相互独立地执行任意操作。也可以在任何时候停止进一步传递请求。而各种装饰器(Decorator)可以扩展对象的行为,同时使其与基本接口保持一致。此外,z装饰器不允许破坏请求流。

代码示例

index.php: 概念示例

<?php

namespace RefactoringGuru\ChainOfResponsibility\Conceptual;

/**
 * The Handler interface declares a method for building the chain of handlers.
 * It also declares a method for executing a request.
 */
interface Handler
{
    public function setNext(Handler $handler): Handler;

    public function handle(string $request): ?string;
}

/**
 * The default chaining behavior can be implemented inside a base handler class.
 */
abstract class AbstractHandler implements Handler
{
    /**
     * @var Handler
     */
    private $nextHandler;

    public function setNext(Handler $handler): Handler
    {
        $this->nextHandler = $handler;
        // Returning a handler from here will let us link handlers in a
        // convenient way like this:
        // $monkey->setNext($squirrel)->setNext($dog);
        return $handler;
    }

    public function handle(string $request): ?string
    {
        if ($this->nextHandler) {
            return $this->nextHandler->handle($request);
        }

        return null;
    }
}

/**
 * All Concrete Handlers either handle a request or pass it to the next handler
 * in the chain.
 */
class MonkeyHandler extends AbstractHandler
{
    public function handle(string $request): ?string
    {
        if ($request === "Banana") {
            return "Monkey: I'll eat the " . $request . ".\n";
        } else {
            return parent::handle($request);
        }
    }
}

class SquirrelHandler extends AbstractHandler
{
    public function handle(string $request): ?string
    {
        if ($request === "Nut") {
            return "Squirrel: I'll eat the " . $request . ".\n";
        } else {
            return parent::handle($request);
        }
    }
}

class DogHandler extends AbstractHandler
{
    public function handle(string $request): ?string
    {
        if ($request === "MeatBall") {
            return "Dog: I'll eat the " . $request . ".\n";
        } else {
            return parent::handle($request);
        }
    }
}

/**
 * The client code is usually suited to work with a single handler. In most
 * cases, it is not even aware that the handler is part of a chain.
 */
function clientCode(Handler $handler)
{
    foreach (["Nut", "Banana", "Cup of coffee"] as $food) {
        echo "Client: Who wants a " . $food . "?\n";
        $result = $handler->handle($food);
        if ($result) {
            echo "  " . $result;
        } else {
            echo "  " . $food . " was left untouched.\n";
        }
    }
}

/**
 * The other part of the client code constructs the actual chain.
 */
$monkey = new MonkeyHandler();
$squirrel = new SquirrelHandler();
$dog = new DogHandler();

$monkey->setNext($squirrel)->setNext($dog);

/**
 * The client should be able to send a request to any handler, not just the
 * first one in the chain.
 */
echo "Chain: Monkey > Squirrel > Dog\n\n";
clientCode($monkey);
echo "\n";

echo "Subchain: Squirrel > Dog\n\n";
clientCode($squirrel);

Output.txt: 执行结构

Chain: Monkey > Squirrel > Dog

Client: Who wants a Nut?
  Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
  Monkey: I'll eat the Banana.
Client: Who wants a Cup of coffee?
  Cup of coffee was left untouched.

Subchain: Squirrel > Dog

Client: Who wants a Nut?
  Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
  Banana was left untouched.
Client: Who wants a Cup of coffee?
  Cup of coffee was left untouched.