精读C++20设计模式——行动型设计模式:责任链
前言
责任链将会是我们的第一个行动型设计模式。我需要强调的是——这一蔟设计模式实际上谈不上有更加集合的设计意图,更多的像是补足程序设计除了创建和静态架构本身,剩下的动态运行交互的部分。责任链就隶属于此。
什么是责任链
责任链是这样的一种追责模式:,我们将待处理的对象投射到一个由处理句柄Processor组成的链条,每一个链条可以处理转发进入的对象,可以拦截对象,可以坐视不管对象,总之,走完这个处理流程,我们就处理了对象。
经典的两种责任链的形式,包含指针链和代理链两种,我们都来仔细品味一下。
指针链(Pointer Chain)
在最基础的实现里,责任链往往是指针串联的。也就是说,每一个处理器(Handler)手里都保存着一个 next
指针,指向链条上的下一个节点。当请求到来时,当前节点先处理(或者决定不处理),然后再把请求交给下一个节点,直到责任链的末尾。
#include <iostream>
#include <memory>
#include <string>
class Handler {
public:
virtual ~Handler() = default;
void set_next(std::shared_ptr<Handler> next) { next_ = next; }
void handle(const std::string& req) {
if (!process(req) && next_) {
next_->handle(req);
}
}
protected:
virtual bool process(const std::string& req) = 0;
private:
std::shared_ptr<Handler> next_;
};
class AuthHandler : public Handler {
protected:
bool process(const std::string& req) override {
if (req == "auth") {
std::cout << "AuthHandler handled\n";
return true;
}
return false;
}
};
class LogHandler : public Handler {
protected:
bool process(const std::string& req) override {
if (req == "log") {
std::cout << "LogHandler handled\n";
return true;
}
return false;
}
};
int main() {
auto auth = std::make_shared<AuthHandler>();
auto log = std::make_shared<LogHandler>();
auth->set_next(log);
auth->handle("log"); // LogHandler handled
auth->handle("auth"); // AuthHandler handled
auth->handle("xxx"); // nobody handles
}
当然,咱们如果不想要按照链表的方式,采用直接映射或者是其他办法,都是可以随意变通的。咱们随时都可以处理和替换上面的链表。但是就需要咱们自己维护。还是有点风险
代理链(Proxy Chain)
另一个常见实现是代理链。它的核心思想是:请求的处理不是节点自己决定“要不要传递”,而是由链式代理类统一调度。每一个节点都被包装成代理,由代理控制“是否调用下一个”。这种方式广泛用于 拦截器(Interceptor)、中间件(Middleware) 等框架中。
#include <iostream>
#include <vector>
#include <functional>
class ProxyChain {
public:
using Handler = std::function<void(ProxyChain&)>;
void add(Handler h) {
handlers_.push_back(std::move(h));
}
void proceed() {
if (index_ < handlers_.size()) {
auto h = handlers_[index_++];
h(*this);
}
}
private:
std::vector<Handler> handlers_;
size_t index_ = 0;
};
int main() {
ProxyChain chain;
chain.add([](ProxyChain& next){
std::cout << "Auth check\n";
next.proceed();
});
chain.add([](ProxyChain& next){
std::cout << "Logging\n";
next.proceed();
});
chain.add([](ProxyChain&){
std::cout << "Final handler\n";
});
chain.proceed();
}
输出:
Auth check
Logging
Final handler
代理链显然要比经典的指针链更加的解耦合,我们来让一个完全的代理框架作为控制的一个中心,节点自己只会去负责自己如何实现问题。而且这样的话,咱们不用关心交底动作的实现,可以请求控制中心自己做这个事情。
异步责任链(Async Chain)
在今天,随着CPU核心的快速增加,显然异步是一个非常值得考虑的要素。这里笔者扩展一下异步责任链,实际上没有什么新鲜的。我们只是和 Promise
或 future
的概念结合。我们可以改造一下:
#include <iostream>
#include <future>
#include <vector>
#include <functional>
class AsyncChain {
public:
using Handler = std::function<std::future<void>()>;
void add(Handler h) { handlers_.push_back(std::move(h)); }
void run() {
std::future<void> fut = std::async(std::launch::async, [this]{
for (auto& h : handlers_) {
h().get(); // 等待每一步完成
}
});
fut.get();
}
private:
std::vector<Handler> handlers_;
};
int main() {
AsyncChain chain;
chain.add([]{
return std::async([]{ std::cout << "Step 1 async\n"; });
});
chain.add([]{
return std::async([]{ std::cout << "Step 2 async\n"; });
});
chain.run();
}
可以看到,我们的每一个AsyncChain还是那种“链式的分发”(当然可以改成单链表穿起来的形式,这个随意,如果我们的起点关心整体,那咱们就使用std::vector或者是std::list统计起来),但是显然我们不直接等结果,而是等待每一个的异步结果。
其他常见设计变种
环形链(Circular Chain)
环形链的思想很直白:责任链的尾部重新连回链头,形成一个闭环。这样,当一个请求流转到末尾时,它并不会自然结束,而是会继续回到第一个处理器。这个特性使得它天然适合需要循环处理的场景,比如调度器或事件广播。
#include <iostream>
#include <memory>
#include <string>
class Handler {
public:
Handler(std::string name): name_(std::move(name)) {}
void set_next(std::shared_ptr<Handler> next) { next_ = next; }
void handle(const std::string& req, int round) {
std::cout << name_ << " processing " << req << " (round " << round << ")\n";
if (round > 0 && next_) {
next_->handle(req, round - 1);
}
}
private:
std::string name_;
std::shared_ptr<Handler> next_;
};
int main() {
auto a = std::make_shared<Handler>("A");
auto b = std::make_shared<Handler>("B");
auto c = std::make_shared<Handler>("C");
a->set_next(b);
b->set_next(c);
c->set_next(a); // 环形
a->handle("task", 5);
}
这里的代码中,链条是 A -> B -> C -> A
的循环,每个节点都会处理任务,并且我们人为设置了 round
来中止循环。环形责任链的迭代意义在于,它从单向链表的形式演化为循环结构,可以持续“传球”直到任务完成或条件满足。
树形链(Tree Chain)
责任链不一定是直线型的,它也可以扩展成一棵树。在 GUI 系统里,事件常常会从根窗口逐层传递到子控件,又可能“冒泡”返回上层,这就是树形责任链的一个典型例子。
#include <iostream>
#include <vector>
#include <memory>
#include <string>
class Node {
public:
Node(std::string name): name_(std::move(name)) {}
void add_child(std::shared_ptr<Node> child) { children_.push_back(child); }
void handle(const std::string& req) {
std::cout << name_ << " received " << req << "\n";
for (auto& child : children_) {
child->handle(req);
}
}
private:
std::string name_;
std::vector<std::shared_ptr<Node>> children_;
};
int main() {
auto root = std::make_shared<Node>("Root");
auto panel = std::make_shared<Node>("Panel");
auto button = std::make_shared<Node>("Button");
auto textbox = std::make_shared<Node>("Textbox");
root->add_child(panel);
panel->add_child(button);
panel->add_child(textbox);
root->handle("click");
}
在这个例子中,请求像水一样“流淌”到所有子节点。代码演化的意义是:我们从链表结构走向树形结构,责任链的扩展性更强,但同时控制流也更复杂。
策略链(Chain + Strategy)
如果我们希望责任链节点更加灵活,可以在运行时决定使用哪种处理逻辑,就会自然引入策略模式。每个节点内部并不绑定具体的处理逻辑,而是持有一个可变的策略。
#include <iostream>
#include <functional>
#include <memory>
class Handler {
public:
Handler(std::function<bool(const std::string&)> strat): strategy_(std::move(strat)) {}
void set_next(std::shared_ptr<Handler> next) { next_ = next; }
void handle(const std::string& req) {
if (!strategy_(req) && next_) {
next_->handle(req);
}
}
private:
std::function<bool(const std::string&)> strategy_;
std::shared_ptr<Handler> next_;
};
int main() {
auto auth = std::make_shared<Handler>([](const std::string& req){
if (req == "auth") { std::cout << "auth ok\n"; return true; }
return false;
});
auto log = std::make_shared<Handler>([](const std::string& req){
if (req == "log") { std::cout << "logged\n"; return true; }
return false;
});
auth->set_next(log);
auth->handle("log");
}
这种演化让责任链更加动态化,策略函数相当于“插拔式”的逻辑。迭代意义是我们不再需要为每个节点单独写一个类,而是将处理逻辑解耦出来,形成可组合的责任链。
可回溯责任链(Reversible Chain)
最后一个变种是支持回退的责任链。当某个节点处理失败时,可以沿着链路回退到前面节点执行补偿操作。这在事务系统里非常常见。
#include <iostream>
#include <vector>
#include <functional>
class ReversibleChain {
public:
using Action = std::function<bool()>; // 正向
using Undo = std::function<void()>; // 反向
void add(Action act, Undo undo) {
steps_.push_back({std::move(act), std::move(undo)});
}
void run() {
size_t executed = 0;
for (; executed < steps_.size(); ++executed) {
if (!steps_[executed].first()) break;
}
if (executed < steps_.size()) {
std::cout << "rollback...\n";
for (int i = executed - 1; i >= 0; --i) {
steps_[i].second();
}
}
}
private:
struct Step {
Action first;
Undo second;
};
std::vector<Step> steps_;
};
int main() {
ReversibleChain chain;
chain.add([]{ std::cout << "step1 ok\n"; return true; },
[]{ std::cout << "undo1\n"; });
chain.add([]{ std::cout << "step2 fail\n"; return false; },
[]{ std::cout << "undo2\n"; });
chain.run();
}
这段代码模拟了事务处理:前两个步骤执行时,第二步失败触发回退机制。它的演化意义是责任链不仅支持“正向传递”,也支持“反向回溯”,增强了鲁棒性。
总结
责任链处理什么问题?
责任链要解决的核心问题是:当请求需要被多个处理者依次尝试时,我们不想在调用方显式地写死“谁来处理”。如果调用方必须了解所有的处理逻辑并逐个调用,那么系统会高度耦合且不利于扩展
责任链如何解决上面的问题?
责任链的解决方式很优雅:把处理逻辑抽象为节点,并把节点通过链式结构组织起来。调用方只需要把请求交给链的起点,链会自动沿着节点传递下去,直到某个节点完成处理或到达终点为止。这样,调用方和具体处理者就彻底解耦,链条的组合方式也可以在运行时自由变化。
各种变种的优劣对比
变种 | 优点 | 缺点 | 典型做法/应用场景 |
---|---|---|---|
指针链 | 实现直观、轻量、灵活替换节点 | 链路长时不易追踪,组织逻辑要手动维护 | 经典的 Handler->set_next() 模式 |
代理链 | 请求调度交给框架统一管理,节点只专注业务逻辑 | 节点失去部分“是否传递”的自主性,依赖代理机制 | 中间件框架、拦截器、过滤器 |
环形链 | 天然适合循环调度和轮询,能保证请求持续传递 | 必须设置中止条件,否则可能陷入无限循环 | 调度器、轮询器、事件广播 |
树形链 | 支持多路分发,能表达复杂的层次化逻辑(如 GUI 事件) | 结构复杂,请求流向和调试难度更高 | GUI 事件冒泡、日志多目的地 |
策略链 | 节点逻辑可动态替换,支持插拔式策略组合 | 层次感增加,逻辑分布在策略函数中,阅读成本提升 | 规则引擎、权限校验 |
异步责任链 | 自然支持异步流程,避免阻塞,适配现代异步框架 | 控制流复杂,需要处理异常、超时和上下文传递 | Web 中间件、IO 任务链 |
可回溯责任链 | 支持回退/补偿操作,适合事务场景,鲁棒性高 | 实现复杂,需要保存上下文和反向操作 | 数据库事务、分布式 Saga、撤销/重做 |