责任链
文章目录
今天是第十二种设计模式: 责任链.
考虑一个典型的公司违规行为:内幕交易。 假设一个特定的交易员被当场抓住利用内幕消息进行交易。 这事该怪谁?如果管理层不知道,那就是交易员了。 但或许交易员的同行也参与其中,在这种情况下,集团经理可能要对此负责。 或者,这种做法是一种制度上的做法,在这种情况下,应该受到指责的是首席执行官。
上面的例子是责任链(Chain of Responsibility, CoR
)的一个示例, 他的核心思想是:将多个处理请求的对象连成一条链,请求沿着链传递,直到有一个对象处理它为止。
这种模式通过解耦请求的发送者和接收者,使多个对象都有机会处理请求,同时可以动态调整处理流程。
责任链模式的关键角色和结构如下:
角色 | 说明 |
---|---|
Handler | 抽象处理者,定义处理请求的接口,通常包含一个指向下一个处理者的引用(nextHandler )。 |
ConcreteHandler | 具体处理者,实现 Handler 接口,决定是否处理请求或传递给下一个处理者。 |
Client | 客户端,负责构建责任链,并触发请求的传递。 |
结构示意图:
请求 → Handler → ConcreteHandler1 → ConcreteHandler2 → ... → 处理成功或终止
场景
我们举一个电脑游戏的例子, 在游戏中每个生物(creature
)都有一个名字和两个属性值(攻击attack
)(防御defense
);
#include <iostream>
#include <string>
using namespace std;
struct Creature {
string name_;
int attack_;
int defense_;
Creature(string&& name, const int attack, const int defense)
: name_{move(name)}, attack_{attack}, defense_{defense} {}
friend ostream& operator<<(ostream& os, const Creature& creature) {
os << "Creature name: " << creature.name_
<< " Creature attack: " << creature.attack_
<< " Creature defense " << creature.defense_
<< std::endl;
return os;
}
};
现在,随着生物在游戏中的进展,它可能会遇到一个道具(例如,一把魔法剑),或者它最终会被施魔法。 在任何一种情况下,它的攻击和防御值都将被CreatureModifier
修改。 此外还有应用多个修改器的情况,所以我们需要能够在生物上堆叠修改器,允许它们按照被附加的顺序应用。
指针链
在经典的责任链(CoR)方式中,我们将实现CreatureModifier
如下:
class CreatureModifier {
private:
std::shared_ptr<CreatureModifier> next_{nullptr};
protected:
Creature& creature_;
public:
explicit CreatureModifier(Creature& creature) : creature_{creature} {}
virtual ~CreatureModifier() = default;
void add(std::shared_ptr<CreatureModifier> cm) {
if (next_) {
next_->add(cm);
} else {
next_ = move(cm);
}
}
// 注意 handle 是从下一个对象开始调用的
// 一般抽象类是根, 具体类对象从第二个开始
virtual void handle() {
if (next_) {
next_->handle();
}
}
};
这里解释下这串代码:
- 这是我们的
Handler
: 抽象处理者, 定义处理的接口, 持有一个要修改对象引用. - 该类获取并存储准备修改的生物的引用。
next_
成员指向一个可选的CreatureModifer
。当然,这意味着它所指向的修改器是CreatureModifer
的继承者。- 添加另一个
CreatureModifer
到修改器链中。这是递归执行的: 如果当前修饰符为nullptr
,则将其设置为该值,否则遍历整个链并将其放到末端(就像是一个简单链表的尾插一样)。 - 函数
handle()
只是处理链中的下一项,如果它存在的话也不会执行和本体相关的行为。它是虚函数,这意味着它需要被重写。
到目前为止,我们得到的只是一个简陋的在尾部追加修改器的单链表实现。
不过当我们开始继承它的时候,事情会变得更清晰。
例如,以下制作一个修改器,使生物的攻击值加倍:
class DoubleAttackModifier final : public CreatureModifier {
public:
explicit DoubleAttackModifier(Creature& creature)
: CreatureModifier{creature} {}
void handle() override {
creature_.attack_ *= 2;
CreatureModifier::handle();
}
};
然后是另一个修改器, 将攻击值为2或2以下的生物的防御能力增加1:
class IncreaseDefenseModifier final : CreatureModifier {
public:
explicit IncreaseDefenseModifier(Creature& creature)
: CreatureModifier{creature} {}
void handle() override {
if (creature_.attack_ <= 2)
creature_.defense_ += 1;
CreatureModifier::handle();
}
};
好的, 我们这样就完成了我们的的具体处理者(ConcreteHandler
), 这两个修改器直接继承自 CreatureModifier
, 在他们的 handle()
方法中就是他们要做的修改: 一个加倍攻击力, 另一个将攻击值为2或2以下的生物的防御能力增加1.
其中最重要的部分是每个继承类都不能忘记在其自己的handle()
实现的末尾调用基类的 handle()
方法, 否则无法形成调用链.
很好, 我们完成了, 最后我们写测试用例将这些组合在一起形成责任链:
void test() {
Creature goblin{ "Goblin", 1, 1 };
CreatureModifier root{ goblin };
DoubleAttackModifier r1{ goblin };
DoubleAttackModifier r1_2{ goblin };
IncreaseDefenseModifier r2{ goblin };
root.add(make_shared<DoubleAttackModifier>(r1));
root.add(make_shared<DoubleAttackModifier>(r1_2));
root.add(make_shared<IncreaseDefenseModifier>(r2));
root.handle();
cout << goblin << endl;
}
将会输出:
Creature name: Goblin
Creature attack: 4
Creature defense 1
这里还有一个附加场景, 假设我们临时对生物施加一个数值无法变化的法术, 这要怎么做?
其实很简单, 只要在对应修改类中将 handle()
写为空实现就可以了, 这样调用链就会在这里停止.
class NoBonusesModifier final : public CreatureModifier {
public:
explicit NoBonusesModifier(Creature& creature) : CreatureModifier(creature) {}
void handle() override {
// 此处空实现!
}
};
现在只需要将该类插在某个节点处, 之后的所有 handle()
都不会被执行了.(可怜的哥布林)
代理链
这里作者说用到了中介者模式和观察者模式, 代码里还用了点 boost
, 我还没看到, 所以我决定偷懒贴原文.
指针链的例子是非常人工的。在现实世界中,你会希望生物能够任意承担和失去加成,这是一个仅追加链表所不支持的。此外,你不想永久地修改底层生物的属性(就像我们做的那样),你想要保持临时修改。
实现CoR的一种方法是通过一个集中的组件。这个组件可以保存游戏中所有可用的修改器列表,并且可以通过确保所有相关的加值被应用来帮助查询特定生物的攻击或防御。
我们将要构建的组件称为事件代理(event broker
)。由于它连接到每个参与组件,因此它表示中介者设计模式,而且,由于它通过事件响应查询,因此它利用了观察者设计模式。
让我们构建一个。首先,我们将定义一个名为Game的结构,它将代表正在玩的游戏:
struct Game { // 中介者
signal<void (Query&)> queries;
};
我们正在使用Boost.Signals2库,用于保存称为queries
的信号。本质上,这让我们做的是发射这个信号(signal
),并由每个插槽solt
(监听组件)处理它。但是事件与查询生物的攻击值或防御值有什么关系呢?
好吧,假设你想要查询一个生物的统计信息。你当然可以尝试读取一个字段,但请记住:我们需要在知道最终值之前应用所有修饰器。因此,我们将把查询封装在一个单独的对象中(这是命令模式),定义如下:
struct Query
{
string creature_name;
enum Argument { attack, defense} argument;
int result;
};
我们在前面提到的类中所做的一切都包含了从生物中查询特定值的概念。我们需要提供的只是生物的名称和我们感兴趣的统计信息。Game::Query
将构建并使用这个值来应用修饰器并返回最终值。
现在,让我们来看看生物的定义。这和我们之前的很相似。在字段方面唯一的区别是Game
的引用:
class Creature
{
Game& game;
int attack, defense;
public:
string name;
Creature(Game& game, ...) : game { game }, ... { ... }
// 其他成员函数
};
现在,注意attack
和defense
是私有的。这意味着,为了获得最终的(后修饰符)攻击值,你需要调用一个单独的getter函数:
int Creature::get_attack() const
{
Query q{name, Query::Argument::attack, attack};
game.queries(q);
return q.result;
}
这就是奇迹发生的地方!我们不只是返回一个值或静态地应用一些基于指针的链,而是使用正确的参数创建一个Query
,然后将查询发送给订阅Game::queries
的任何人处理。每个订阅组件都有机会修改基线攻击值。
现在让我们来实现修改器。同样,我们将创建一个基类,但这一次它没有handle()
方法:
class CreatureModifier:
{
Game& game;
Creature& creature;
public:
CreatureModifier(Game& game, Creature& creature) :
game(game), creature(creature)
{}
};
此修饰器基类并不是特别有趣。实际上,你完全可以不使用它,因为它所做的只是确保使用正确的参数调用构造函数。但是由于我们已经使用了这种方法,现在让我们继承CreatureModifier
,看看如何执行实际的修改:
class DoubleAttackModifier : public CreatureModifier
{
connection conn;
public:
DoubleAttackModifier(Game& game, Creature& creature)
: CreatureModifier(game, creature)
{
conn = game.queries.connect([&](Query& q){
if (q.creature_name == creature.name &&
q.argument == Query::Argument::attack)
q.result *= 2;
});
}
~DoubleAttackModifier() { conn.disconnect(); }
};
如您所见,所有的乐趣都发生在构造函数和析构函数中;不需要其他方法。在构造函数中,我们使用Game
引用获取Game::queries
信号并连接到它,指定一个lambda
表达式使攻击加倍。当然,lambda
表达式必须做一些检查:我们需要确保我们增加了正确的生物(我们通过名称进行比较),并且我们所追求的统计数据实际上是attack
。这两条信息都保存在查询引用中,就像我们修改的初始结果值一样。
我们还注意存储信号连接,以便在对象被销毁时断开它。这样,我们可以暂时应用修改器,让它在修改器超出作用域时失效。
把它们放在一起,我们得到以下结果:
Game game;
Creature goblin{ game, "Strong Goblin", 2, 2 };
cout << goblin << endl;
// name: Strong Goblin attack: 2 defense: 2
{
DoubleAttackModifier dam{ game, goblin };
cout << goblin << endl;
// name: Strong Goblin attack: 4 defense: 2
}
cout << goblin << endl;
// name: Strong Goblin attack: 2 defense: 2
这里发生了什么事?在被修改之前,地精是2/2。然后,我们制造一个范围,其中地精受到双重攻击修改器的影响,所以在范围内它是一个4/2的生物。一旦退出作用域,修改器的析构函数就会触发,并断开自己与代理的连接,因此在查询值时不再影响它们。因此,地精本身再次恢复为2/2的生物。
总结
责任链模式的核心思想
责任链模式允许将多个处理对象(Handler)链接成链,请求沿链传递,直到被某个处理对象处理。其核心是解耦请求发送者和接收者,支持动态调整处理逻辑。
何时需要使用责任链模式?
责任链模式的核心应用场景是 动态组织多个对象处理同一请求,具体需求场景包括:
- 多级处理逻辑:
- 请求需要经过多个处理者按顺序处理,且处理者的顺序或组合可能动态变化。
- 示例:审批流程(员工→经理→CEO)、HTTP中间件链、异常处理链。
- 解耦请求与处理者:
- 客户端无需知道具体由哪个对象处理请求,只需将请求提交到链的起点。
- 示例:日志系统根据日志级别自动选择处理器(DEBUG→INFO→ERROR)。
- 动态扩展处理逻辑:
- 在不修改现有代码的情况下,灵活添加或移除处理者。
- 示例:电商订单处理链(库存校验→支付验证→物流分配)。
责任链模式解决的核心问题
- 紧耦合问题:
- 避免请求发送者与具体处理者的直接依赖,降低系统耦合度。
- 单一职责原则:
- 每个处理者只关注自己能处理的逻辑,职责清晰。
- 灵活性与可扩展性:
- 动态调整处理链顺序或新增处理者,无需修改客户端代码。
与其他设计模式的协同使用
责任链模式常与其他模式结合,以构建更复杂的系统逻辑:
模式 | 协同场景 | 示例 |
---|---|---|
命令模式 | 将请求封装为命令对象,沿责任链传递,处理者执行具体操作。 | 审批系统中,命令对象携带审批数据,责任链中的处理者执行审批或驳回操作。 |
组合模式 | 处理链中的节点可以是组合对象,处理请求或递归传递给子节点。 | 文件系统中,目录(组合节点)和文件(叶子节点)共同处理“搜索”请求。 |
策略模式 | 责任链中的处理者通过策略模式选择不同的处理逻辑。 | 日志处理链中,不同策略处理不同格式的日志(JSON、文本)。 |
观察者模式 | 处理链的完成或中断触发观察者的通知逻辑。 | 订单处理链完成后,通知用户和库存系统更新状态。 |
与其他模式的对比
- 装饰器模式:
- 装饰器:通过嵌套包装增强对象功能,所有装饰器均处理请求。
- 责任链:请求可能被链中任意一个处理者截断,不一定传递到底。
- 状态模式:
- 状态模式:对象行为随内部状态改变。
- 责任链:对象行为由外部链式处理者决定。
经典应用场景
- Web中间件:
- Express.js/Koa的中间件链,依次处理HTTP请求(身份验证→日志→路由转发)。
- 事件冒泡:
- 浏览器事件处理机制(子元素→父元素→文档根节点)。
- 工作流引擎:
- 订单处理流程(库存检查→支付→发货→通知)。
- 异常处理:
- Java异常捕获机制(try→catch→finally)。
实现步骤与关键点
- 定义处理者接口:
- 声明处理方法(如
handleRequest()
)和设置下一处理者的方法(如setNext()
)。
- 声明处理方法(如
- 实现具体处理者:
- 每个处理者决定是否处理请求或传递给下一节点。
- 构建处理链:
- 客户端按需组装处理链(如
handlerA.setNext(handlerB).setNext(handlerC)
)。
- 客户端按需组装处理链(如
注意事项
- 链的完整性:确保请求最终被处理或明确终止,避免无限传递。
- 性能优化:链过长可能影响性能,需合理设计处理者顺序(高频处理者优先)。
- 避免循环引用:链的构造需逻辑清晰,防止形成闭环。
总结
责任链模式是 动态分发的流水线,其核心价值在于:
- 解耦与灵活性:分离请求发送者与处理者,支持动态调整处理流程。
- 可扩展性:新增处理者无需修改现有代码,符合开闭原则。
- 职责清晰:每个处理者专注于单一任务,提升代码可维护性。
适用于需要多级处理、动态流程控制的场景,是现代中间件框架和复杂工作流系统的核心设计模式之一。