天天看点

经典行为型设计模式:责任链模式

作者:风趣生命的起源

意图

责任链模式是一种行为设计模式,它允许你将请求沿着一条处理器链传递。当接收到一个请求时,每个处理器都决定是处理该请求还是将其传递给链中的下一个处理器。

经典行为型设计模式:责任链模式

问题

想象一下,你正在开发一个在线订购系统。你希望只有经过身份验证的用户才能创建订单,而具有管理员权限的用户必须对所有订单拥有完全访问权限。

经过一番规划,你意识到这些检查必须按顺序执行。每当应用程序收到包含用户凭据的请求时,它可以尝试对用户进行身份验证。然而,如果这些凭据不正确,身份验证失败,就没有继续进行任何其他检查的必要了。

经典行为型设计模式:责任链模式

在订单系统自身处理请求之前,该请求必须通过一系列的检查。

在接下来的几个月中,您实施了几个顺序检查。

其中一位同事建议,直接将原始数据传递给订单系统是不安全的。因此,您添加了一个额外的验证步骤来清理请求中的数据。

后来,有人注意到系统容易受到暴力破解密码的攻击。为了解决这个问题,您立即添加了一个检查,过滤掉来自同一IP地址的重复失败请求。

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

经典行为型设计模式:责任链模式

代码检查的逻辑越来越复杂,随着每个新功能的添加,代码变得越来越臃肿。修改一个检查有时会影响其他检查。最糟糕的是,当您尝试重用这些检查来保护系统的其他组件时,由于这些组件需要其中一些检查但不需要全部检查,您不得不复制部分代码。

系统变得非常难以理解,维护成本也很高。您在代码中苦苦挣扎了一段时间,直到有一天决定对整个代码进行重构。

解决方法

与许多其他行为设计模式类似,责任链模式依赖于将特定的行为转化为独立的对象,称为处理器(handlers)。在我们的例子中,每个检查应该被提取为自己的类,具有执行检查的单个方法。请求及其数据作为参数传递给该方法。

该模式建议将这些处理器链接成一条链。每个链接的处理器都有一个字段用于存储对链中下一个处理器的引用。处理器除了处理请求之外,还将请求传递到链中的下一个处理器。请求沿着链传递,直到所有处理器都有机会处理它。

最重要的是:处理器可以决定不将请求传递给链中的下一个处理器,从而有效地停止任何进一步的处理。

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

经典行为型设计模式:责任链模式

处理器一个接一个地排列,形成一条链。

然而,还有一种稍微不同的方法(更加符合规范),其中处理器在接收到请求时决定是否可以处理它。如果可以处理,它不会将请求传递给下一个处理器。因此,要么只有一个处理器处理请求,要么根本没有处理器处理。这种方法在处理图形用户界面中的元素堆栈中的事件时非常常见。

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

经典行为型设计模式:责任链模式

一条链可以从对象树的一个分支形成。

所有处理程序类实现相同的接口非常重要。每个具体的处理程序只需要关注下一个处理程序是否具有`execute`方法。通过这种方式,您可以在运行时组合链,使用各种处理程序,而不将代码与它们的具体类耦合在一起。

真实世界类比

经典行为型设计模式:责任链模式

打给技术支持的电话可能要经过多个操作员的转接。

您刚刚在计算机上购买并安装了一件新的硬件设备。由于您是个极客,计算机上安装了几个操作系统。您尝试启动所有操作系统,以查看是否支持该硬件。Windows会自动检测并启用硬件。然而,您心爱的Linux拒绝与新硬件配合工作。带着一丝希望,您决定拨打盒子上写着的技术支持电话号码。

第一件事,您听到的是自动回复器的机器人声音。它提供了九种常见问题的解决方案,但与您的情况无关。过了一会儿,机器人将您连接到了一位现场操作员。

不幸的是,操作员也不能提供具体的建议。他不停地引用手册中的冗长摘录,拒绝倾听您的意见。在第10次听到“您尝试关闭并重新打开计算机吗?”这句话后,您要求与一位真正的工程师联系。

最终,操作员将您的电话转接给了一位工程师,他可能在黑暗的地下室里的孤寂服务器房里已经渴望与人进行实时聊天好几个小时了。工程师告诉您在哪里下载适用于Linux的正确驱动程序以及如何安装它们。终于找到解决方案了!您挂断电话时,心中充满了喜悦。

结构

经典行为型设计模式:责任链模式

1、处理器(Handler)声明了一个接口,所有具体处理器都要遵循该接口。通常,它只包含一个方法用于处理请求,但有时也可能包含另一个方法用于设置链上的下一个处理器。

2、基础处理器(Base Handler)是一个可选的类,您可以将通用的样板代码放在其中。

通常,这个类定义了一个字段,用于存储对下一个处理器的引用。客户端可以通过将处理器传递给前一个处理器的构造函数或设置器来构建链。该类还可以实现默认的处理行为:在检查下一个处理器是否存在后,可以将执行传递给下一个处理器。

3、具体处理器(Concrete Handlers)包含了实际处理请求的代码。每个处理器在接收到请求时必须决定是否处理它,以及是否将其传递给链上的下一个处理器。

处理器通常是自包含的且不可变的,它们通过构造函数一次性接收所有必要的数据。

4、客户端(Client)可以只在一开始组合链,也可以根据应用程序的逻辑动态地组合链。请注意,请求可以发送到链上的任何处理器,不一定是第一个处理器。

伪代码

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

经典行为型设计模式:责任链模式

GUI类使用组合模式构建。每个元素与其容器元素相连接。构建一个由元素本身开始并通过其所有容器元素的链。

应用程序的GUI通常以对象树的形式组织结构。例如,渲染应用程序主窗口的Dialog类将成为对象树的根节点。对话框包含面板(Panel),面板可能包含其他面板或简单的低级元素,如按钮(Button)和文本字段(TextField)。

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

经典行为型设计模式:责任链模式

这就是帮助请求在 GUI 对象中传递的方式。

当用户将鼠标光标悬停在一个元素上并按下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()           

适用范围

使用责任链模式当你的程序需要以不同的方式处理不同类型的请求,但具体的请求类型和处理顺序在事先是未知的。

该模式允许你将多个处理器链接成一个链,并在接收到请求时,逐个“询问”每个处理器是否能处理该请求。这样,所有的处理器都有机会处理该请求。

在需要按照特定顺序执行多个处理器的情况下使用该模式是很重要的。

由于你可以以任意顺序链接处理器到链中,所有的请求将按照你的计划准确地通过整个链。

在运行时需要改变处理器集合和它们的顺序时,使用责任链模式。

如果在处理器类中提供了引用字段的设置器,你就能够动态地插入、删除或重新排序处理器。

如何实现

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

决定客户端如何将请求数据传递到方法中。最灵活的方式是将请求转换为对象,并将其作为参数传递给处理方法。

2、为了消除具体处理器中重复的样板代码,可以创建一个抽象基本处理器类,派生自处理器接口。

该类应该有一个字段来存储链中下一个处理器的引用。考虑将该类设计为不可变的。然而,如果计划在运行时修改链,就需要为修改引用字段的值定义一个设置器。

还可以实现处理方法的便捷默认行为,即将请求转发给下一个对象,除非没有剩余的处理器。具体处理器可以通过调用父级方法来使用这种行为。

3、逐个创建具体的处理器子类并实现它们的处理方法。每个处理器在接收到请求时应该做出两个决策:

  • 是否处理请求。
  • 是否将请求传递给链中的下一个处理器。

4、客户端可以自行组装链,也可以从其他对象接收预先构建的链。在后一种情况下,你需要实现一些工厂类来根据配置或环境设置构建链。

5、客户端可以触发链中的任何处理器,而不仅仅是第一个处理器。请求将沿着链传递,直到某个处理器拒绝继续传递或者到达链的末尾。

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

  • 链可能只包含一个链接。
  • 某些请求可能无法到达链的末尾。
  • 其他请求可能到达链的末尾但没有被处理。

Python示例

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Optional


class Handler(ABC):
    """
    The Handler interface declares a method for building the chain of handlers.
    It also declares a method for executing a request.
    """

    @abstractmethod
    def set_next(self, handler: Handler) -> Handler:
        pass

    @abstractmethod
    def handle(self, request) -> Optional[str]:
        pass


class AbstractHandler(Handler):
    """
    The default chaining behavior can be implemented inside a base handler
    class.
    """

    _next_handler: Handler = None

    def set_next(self, handler: Handler) -> Handler:
        self._next_handler = handler
        # Returning a handler from here will let us link handlers in a
        # convenient way like this:
        # monkey.set_next(squirrel).set_next(dog)
        return handler

    @abstractmethod
    def handle(self, request: Any) -> str:
        if self._next_handler:
            return self._next_handler.handle(request)

        return None


"""
All Concrete Handlers either handle a request or pass it to the next handler in
the chain.
"""


class MonkeyHandler(AbstractHandler):
    def handle(self, request: Any) -> str:
        if request == "Banana":
            return f"Monkey: I'll eat the {request}"
        else:
            return super().handle(request)


class SquirrelHandler(AbstractHandler):
    def handle(self, request: Any) -> str:
        if request == "Nut":
            return f"Squirrel: I'll eat the {request}"
        else:
            return super().handle(request)


class DogHandler(AbstractHandler):
    def handle(self, request: Any) -> str:
        if request == "MeatBall":
            return f"Dog: I'll eat the {request}"
        else:
            return super().handle(request)


def client_code(handler: Handler) -> None:
    """
    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.
    """

    for food in ["Nut", "Banana", "Cup of coffee"]:
        print(f"\nClient: Who wants a {food}?")
        result = handler.handle(food)
        if result:
            print(f"  {result}", end="")
        else:
            print(f"  {food} was left untouched.", end="")


if __name__ == "__main__":
    monkey = MonkeyHandler()
    squirrel = SquirrelHandler()
    dog = DogHandler()

    monkey.set_next(squirrel).set_next(dog)

    # The client should be able to send a request to any handler, not just the
    # first one in the chain.
    print("Chain: Monkey > Squirrel > Dog")
    client_code(monkey)
    print("\n")

    print("Subchain: Squirrel > Dog")
    client_code(squirrel)           

继续阅读