拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C++装饰器模式变体实战:从std::function到模板混入与CRTP

1. 装饰器模式在C里的独特处境1.1 从GoF经典定义说起装饰器模式Decorator Pattern是GoF二十三个经典模式里我认为“概念最简单、落地最折腾”的一个。它的原始意图就一句话在不修改原有类的前提下动态地给对象添加职责。Java里典型写法是定义一个接口然后写一个抽象装饰器持有被装饰对象的引用再通过子类一层层包上去。C里刚接触这个模式时很多人会照搬这套写法结果写出来一堆虚函数、抽象基类、构造函数传引用看着像模像样实际用起来总觉得别扭。别扭的根源在于C不是一个纯面向对象语言它有值语义、模板、RAII、移动语义这些Java里没有的东西。如果只把装饰器理解成“继承同一接口内部再持有一个同接口对象”那在C里你至少会撞上三个问题一是虚函数调用有开销虽然大多数时候可以忽略但高性能场景受不了二是对象生命周期管理麻烦装饰器持有的是引用还是指针谁负责释放被装饰对象提前析构了怎么办三是一旦需要装饰的方法多了每个装饰器子类都要转发所有接口方法代码冗长到让人怀疑人生。所以在C社区里真正被广泛使用的“装饰器”早就不是GoF原版了而是演化出了各种变体。这些变体利用了模板、lambda、std::function、CRTP等C特有的工具把“动态添加职责”这个核心思想保留下来同时绕开了经典实现里的坑。这篇文章我就把实际项目里踩过的、见过的几种装饰器变体摊开聊一聊每种都给出能直接抄的代码再讲清楚各自的适用场景和隐含成本。1.2 为什么C里经典装饰器常被“嫌弃”先别急着否定经典装饰器它在某些场景下依然有价值比如你需要在运行时动态决定装饰器的组合顺序或者你的代码库已经全面采用了虚函数多态风格。但C开发者普遍“嫌弃”它不是因为模式本身有问题而是因为C有更好的替代手段。最大的问题是“接口污染”。假设你有一个IImageProcessor接口里面有process()、getMetadata()、setRegion()等七八个方法。你写一个WatermarkDecorator只想在process()前后加水印但因为这个类要实现接口它必须把其他几个方法全部转发给被装饰对象。更烦的是如果以后接口加了新方法所有装饰器类都要同步改这违反了开闭原则。Java里可以用动态代理解决C里没有内置动态代理只能靠代码生成或者模板。其次经典装饰器是“对象级”的它包装的是对象。但在很多场景里我们想装饰的是“函数逻辑”不是整个对象。比如给某个业务函数加日志、加耗时统计、加重试用对象装饰器去包装会非常笨重——你得先定义一个函数对象接口再写装饰器类持有被包装的函数对象这相当于把函数调用重新塞回面向对象的壳子里。于是C程序员的直觉是既然要装饰的是一段可调用逻辑直接用std::function和lambda多好既然要装饰的是一个具体类的能力直接用模板在编译期把新行为混进去多好。这些思路发展出了C特有的装饰器变体下面逐一拆解。2. 四种值得上手的装饰器变体2.1 经典对象组合变体接口包装器先说说我实际项目中仍然会用的“改良版”经典装饰器。它没有完全抛弃GoF思路但做了三个关键调整。第一个调整装饰器只实现被装饰接口的可选子集缺失的方法用std::function或std::any兜底。严格来说这打破了接口隔离原则但C允许你在装饰器里持有std::function成员通过构造函数注入“扩展逻辑”而不是通过继承强制实现所有接口方法。比如class ILogger { public: virtual ~ILogger() default; virtual void log(std::string_view msg) 0; }; class TimestampDecorator : public ILogger { public: explicit TimestampDecorator(std::unique_ptrILogger next) : next_(std::move(next)) {} void log(std::string_view msg) override { auto now std::chrono::system_clock::now(); std::ostringstream oss; oss [ std::chrono::system_clock::to_time_t(now) ] msg; next_-log(oss.str()); } private: std::unique_ptrILogger next_; };这里用unique_ptr管理生命周期装饰器拥有被装饰对象的所有权调用链结束时自动释放避免了裸指针的悬垂问题。这是我认为经典装饰器在C里最合理的形态所有权明确、接口简单、销毁安全。第二个调整把“装饰多个方法”改为“只装饰一个核心方法”。如果一个接口确实需要装饰多个方法我不会写一个装饰器去转发所有方法而是把每个可装饰点抽象成独立的扩展接口比如ILoggable、ITimeMeasurable再用多重继承组合。这其实是一种接口隔离 装饰的组合思路。第三个调整用std::shared_ptr代替unique_ptr当装饰器链需要在多个线程或组件间共享时。但注意shared_ptr会引入引用计数的原子操作开销而且循环引用一旦出现就内存泄漏。所以我默认用unique_ptr除非确实需要共享。这个变体的适用场景很清晰团队中其他成员熟悉Java风格代码库已经大量使用虚函数多态并且装饰的目标确实是一个完整的对象行为链类似中间件管道。如果只是给一个函数加一层逻辑别用它。2.2 模板混入变体编译期装饰模板混入Mixin是C里最具“原生气息”的装饰器变体。它不依赖虚函数而是通过模板参数把“被装饰类”作为基类让装饰类继承它并覆盖或扩展其方法。这种方式在编译期就完成了装饰运行时零虚函数开销、零指针间接跳转。一个典型例子template typename Base class LoggingMixin : public Base { public: using Base::Base; // 继承构造函数 void process() { std::cout before process std::endl; Base::process(); std::cout after process std::endl; } };使用方式class BasicProcessor { public: void process() { /* 核心逻辑 */ } }; using LoggedProcessor LoggingMixinBasicProcessor;这里的LoggedProcessor是一个具体类型它拥有BasicProcessor的所有能力同时process()前后加了日志。装饰行为完全发生在编译期运行时性能等同手写代码。但这个变体有两个非常隐蔽的坑。第一个坑是“函数同名覆盖”问题如果Base中有多个重载的process而装饰器只重写了一个版本其他重载会被隐藏导致调用出错。解决办法是在装饰器里加using Base::process;把基类的所有重载暴露出来。第二个坑是模板混入只能“静态装饰”。你在运行时不能动态替换装饰层因为类型在编译期就已经固定。如果两层装饰都是模板混入你没法像经典装饰器那样在运行时决定“先日志后计时”还是“先计时后日志”。当然你可以通过不同嵌套顺序声明多个类型但它们是不同类不能在同一个变量里互换。模板混入最大的价值是“零成本抽象”。在性能敏感的代码里比如游戏引擎的渲染管线、高频交易系统用混入代替虚函数装饰器能省掉每次虚调用的开销同时让编译器有内联的机会。我见过一个网络库把所有编解码步骤都做成混入装饰器层层嵌套最终生成的代码和手写流程一样高效。2.3 std::function lambda 变体运行时函数装饰如果说模板混入是编译期装饰的代表那std::function lambda就是运行时函数装饰的标杆。它的中心思想非常直白把“要装饰的函数”当作可调用对象传给装饰函数装饰函数内部做一些前置/后置处理再调用原始函数。最简洁的实现template typename Func auto with_logging(Func f) { return [f std::forwardFunc(f)](auto... args) mutable { std::cout before std::endl; if constexpr (std::is_void_vstd::invoke_result_tFunc, decltype(args)...) { f(std::forwarddecltype(args)(args)...); std::cout after std::endl; } else { auto result f(std::forwarddecltype(args)(args)...); std::cout after std::endl; return result; } }; }这个模板接受任意可调用对象返回一个新的lambda。新lambda转发所有参数调用原函数并在前后打印日志。用的时候auto process_with_log with_logging(process); process_with_log(42);如果要叠加多个装饰直接嵌套即可auto process_with_log_and_metric with_metric(with_logging(process));但直接嵌套有类型问题外层装饰器返回的lambda类型和内层不一样组合起来比较复杂。更实用的是统一装箱成std::functionusing ProcessFunc std::functionvoid(int); ProcessFunc make_logged(ProcessFunc inner) { return [inner std::move(inner)](int x) { std::cout before std::endl; inner(x); std::cout after std::endl; }; } ProcessFunc make_metric(ProcessFunc inner) { return [inner std::move(inner)](int x) { auto start std::chrono::steady_clock::now(); inner(x); auto end std::chrono::steady_clock::now(); std::cout elapsed: std::chrono::duration_caststd::chrono::microseconds(end - start).count() us std::endl; }; } ProcessFunc f make_metric(make_logged(process)); f(42);这里的std::function起到了“统一类型”的作用让装饰器可以像积木一样自由组合。代价是每次调用会有一次虚函数/类型擦除的间接跳转性能比直接调用慢一个数量级但绝大多数业务逻辑完全不在乎这几十纳秒。它的优势是极强的灵活性你可以在运行时动态选择装饰器组合可以保存装饰器的栈/队列可以把装饰后的函数作为参数传来传去。我用这个变体写过一个小型中间件风格的请求处理器请求进来先经过鉴权、再经过限流、再经过日志、最后到达业务函数每个中间件就是一个std::function装饰器。新增一个中间件只需要写一个函数改动成本极低。2.4 CRTP静态装饰变体零成本抽象CRTPCuriously Recurring Template Pattern是C模板独有的模式基类模板接受派生类作为模板参数。用在装饰器上可以实现“静态多态装饰”既像模板混入那样零成本又能更优雅地让装饰器访问被装饰对象的公开接口。标准写法template typename Derived struct DecoratorBase { void process() { static_castDerived*(this)-beforeProcess(); static_castDerived(*this).processImpl(); static_castDerived*(this)-afterProcess(); } }; struct MyProcessor : DecoratorBaseMyProcessor { void beforeProcess() { std::cout before std::endl; } void processImpl() { std::cout core std::endl; } void afterProcess() { std::cout after std::endl; } };这里的DecoratorBase通过static_cast调用派生类的方法没有虚函数没有vptr编译器可以轻松内联。这种变体的优势在于装饰行为被写成模板基类的一部分派生类只需要实现切面方法before/after主流程由基类统一编排。相当于用继承组合了“装饰器”和“被装饰类”的角色被装饰类本身就是装饰器的实现者。但CRTP装饰器有个典型的困惑它到底是在装饰谁其实被装饰的“原始逻辑”就是派生类的processImpl而DecoratorBase提供了“装饰框架”。如果你想在多个不同业务类上复用同一套日志装饰逻辑你可以让它们都继承同一个LoggingDecoratorDerived模板。这时模板参数Derived会取代业务类的过程名称业务类只需要实现指定的接口。CRTP适合那些“装饰逻辑固定、业务逻辑多变”的场景。比如你要给所有数据校验器加一个“校验失败重试3次再抛异常”的统一行为用CRTP写一个RetryingDecoratorT让所有校验器继承它并实现validateOnce()就能在编译期获得完整的重试逻辑且没有虚函数开销。3. 实操实现一个可扩展的日志装饰器框架3.1 需求场景与接口设计前面讲了四种变体纸上谈兵没意思接下来我把一个真实需求完整走一遍。设想一个简单的缴费服务有一个PaymentService类提供pay(double amount) - bool方法。现在我们需要在不修改PaymentService的情况下给它加上三种能力日志记录每次支付的请求金额和结果。耗时统计打印每次支付的耗时。重试失败之后最多重试3次。而且这三个能力要能自由组合有时候只要日志有时候要日志耗时有时候三者都要甚至顺序可变。这个需求如果用经典GoF装饰器来做需要定义接口、抽象装饰器、三个具体装饰器再手动组合嵌套。代码量不少而且重试装饰器需要处理返回值耗时装饰器要处理异常写起来很啰嗦。用了C变体之后整体会简洁得多。我先设计统一的“可调用原语”。既然pay是一个接受double返回bool的函数那每个装饰器本质上是一个std::functionbool(double)的转换器。定义using PayFunc std::functionbool(double);然后三个装饰器函数分别接收一个PayFunc返回一个新的PayFunc。这样组合时只需要在参数上层层包裹。3.2 基于std::function的装饰器实现日志装饰器PayFunc add_logging(PayFunc inner) { return [inner std::move(inner)](double amount) - bool { std::cout [LOG] paying amount: amount std::endl; bool result inner(amount); std::cout [LOG] pay result: std::boolalpha result std::endl; return result; }; }耗时统计装饰器PayFunc add_metric(PayFunc inner) { return [inner std::move(inner)](double amount) - bool { auto start std::chrono::steady_clock::now(); bool result inner(amount); auto end std::chrono::steady_clock::now(); auto ms std::chrono::duration_caststd::chrono::milliseconds(end - start).count(); std::cout [METRIC] pay took ms ms std::endl; return result; }; }重试装饰器这个稍微复杂点PayFunc add_retry(PayFunc inner, int max_retry 3) { return [inner std::move(inner), max_retry](double amount) - bool { for (int attempt 1; ; attempt) { if (inner(amount)) { return true; } if (attempt max_retry) { return false; } std::cout [RETRY] attempt attempt failed, retrying... std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(100 * attempt)); } }; }组合起来class PaymentService { public: bool pay(double amount) { // 模拟业务逻辑金额大于50则成功 if (amount 50.0) { std::cout [PAYMENT] processing amount std::endl; return true; } std::cout [PAYMENT] insufficient amount: amount std::endl; return false; } }; int main() { PaymentService svc; PayFunc raw [svc](double amount) { return svc.pay(amount); }; PayFunc logged add_logging(raw); PayFunc logged_and_metric add_metric(logged); PayFunc full add_retry(logged_and_metric, 3); full(30.0); full(100.0); }这段代码的优点是每个装饰器只负责一件事想改变组合顺序只需要调整外层嵌套方式。比如想先统计耗时再记录日志就add_logging(add_metric(raw))。想只重试不日志就add_retry(raw)。完全契合开闭原则。有个地方要特别注意raw用lambda捕获了svc的引用所以svc的生命周期必须比full长。实际项目里如果被捕获对象生命周期不确定建议捕获shared_ptr或unique_ptr。我在第三版代码里吃过一个亏捕获了一个临时对象lambda悬空后崩溃排查了好久才发现是生命周期问题。3.3 基于模板混入的编译期装饰实现同样的场景如果希望零运行时开销并且组合在编译期固定就用模板混入。先定义基础业务类class PaymentService { public: bool pay(double amount) { if (amount 50.0) { std::cout [PAYMENT] processing amount std::endl; return true; } std::cout [PAYMENT] insufficient amount: amount std::endl; return false; } };注意这里没有虚函数就是一个普通类。然后定义三个装饰器模板每个都继承模板参数Base并在pay前后加入自己的逻辑template typename Base class LoggingDecorator : public Base { public: using Base::Base; bool pay(double amount) { std::cout [LOG] paying amount: amount std::endl; bool result Base::pay(amount); std::cout [LOG] pay result: std::boolalpha result std::endl; return result; } }; template typename Base class MetricDecorator : public Base { public: using Base::Base; bool pay(double amount) { auto start std::chrono::steady_clock::now(); bool result Base::pay(amount); auto end std::chrono::steady_clock::now(); auto ms std::chrono::duration_caststd::chrono::milliseconds(end - start).count(); std::cout [METRIC] pay took ms ms std::endl; return result; } }; template typename Base class RetryDecorator : public Base { public: using Base::Base; bool pay(double amount) { for (int attempt 1; ; attempt) { if (Base::pay(amount)) { return true; } if (attempt max_retry_) { return false; } std::cout [RETRY] attempt attempt failed, retrying... std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(100 * attempt)); } } private: int max_retry_ 3; };使用的时候通过类型别名定义组合后的类型using FullService LoggingDecoratorMetricDecoratorRetryDecoratorPaymentService; int main() { FullService svc; svc.pay(30.0); svc.pay(100.0); }这个变体里我要强调两个细节。第一个是using Base::Base;它把基类的构造函数继承过来这样FullService可以直接用PaymentService的构造函数参数构造。如果装饰器自己的构造函数需要额外参数比如重试次数就得自己写构造函数并在初始化列表里转发参数。第二个是pay的调用链LoggingDecorator里的Base::pay实际调用的是MetricDecorator::payMetricDecorator里的Base::pay调用RetryDecorator::pay最后到达PaymentService::pay。每一层都是直接调用没有虚函数表编译器可以全部内联。编译期装饰的缺陷刚才提过现在展示一次你没法在运行时让FullService和LoggingDecoratorPaymentService互相替换。它们没有继承关系。如果你需要一种“既能全功能又不想要重试”的可变组合模板混入会让你写出N个类型别名比如using S1 LoggingDecoratorPaymentService; using S2 MetricDecoratorPaymentService; using S3 LoggingDecoratorMetricDecoratorPaymentService;越组合越爆炸。所以当组合需求不确定、需要运行时配置时我会坚定选std::function版本。当组合在编译期确定、性能要求苛刻时才选模板混入。3.4 两种实现的对比与选择建议这里把两种实现放在一起对比方便项目选型。维度std::function lambda模板混入组合时机运行时动态组合编译期静态组合运行时开销每次调用有间接跳转和类型擦除无额外开销可内联代码量少每个装饰器一个函数中每个装饰器一个类模板可读性较好组合式调用直观一般类型别名嵌套深调试难度调用栈多一层std::function复杂场景难追模板错误信息极其冗长编译报错难懂二进制体积无显著膨胀每个组合都会实例化一份代码组合多了膨胀依赖管理依赖std::function和捕获的生命周期依赖模板推导和继承构造函数我的经验法则如果这是一个业务逻辑模块性能不是第一优先级优先用std::function变体。它更灵活更容易单元测试因为装饰器是普通函数可以直接传入mock的PayFunc。如果这是一个底层基础设施或热路径代码比如每帧渲染调用、高频网络收发优先用模板混入。少一次间接跳转多一次内联机会对整体性能有明显帮助。如果团队里新手多尽量用std::function版本。模板混入的编译报错能把人劝退而且继承构造函数的一些边界行为很容易踩坑。4. 常见问题与排查技巧实录4.1 虚函数开销与对象生命周期陷阱经典装饰器最常见的两个问题。第一个问题是“过度使用虚函数”。有些团队把装饰器模式奉为圭臬一个很小的接口也要先定义纯虚基类再写N个装饰器。结果每次调用都要两次虚函数跳转装饰器被装饰器虽然单次开销可能只有几纳秒但在高频循环里累积起来很可观。排查方法很简单用perf top或者火焰图看热点是否落在__cxa_pure_virtual或虚调用相关符号上。如果确实遇到考虑把装饰器改成模板混入。第二个问题是对象生命周期。经典装饰器的构造通常需要传入被装饰对象很多新手用裸指针传引用class Decorator { public: Decorator(ILogger* inner) : inner_(inner) {} private: ILogger* inner_; };然后他写vectorunique_ptrILogger loggers; loggers.push_back(make_uniqueTimestampDecorator(new ConsoleLogger())); // 危险更危险的是持有局部变量引用ILogger* logger new ConsoleLogger(); TimestampDecorator decorator(logger); delete logger; decorator.log(hello); // 悬垂指针我的建议是凡是装饰器对被装饰对象拥有所有权就使用unique_ptr凡是只借用不拥有必须明确文档说明“调用方保证被装饰对象在装饰器生命周期内有效”最好用shared_ptr持有避免手动管理。没有第三种选择除非你能接受定时炸弹。4.2 模板装饰器导致的代码膨胀模板混入装饰器有一个隐蔽问题每组合一种类型编译器就会生成一套完整的代码。如果你有10个基础功能类10个装饰器模板用户随意组合潜在的类型数量是指数级。即便只用了其中的20种组合二进制里也会出现20份几乎一模一样的pay流程代码。我遇到过真实案例项目链接后二进制体积从20MB涨到60MB排查发现就是某个通用装饰器模板被多处使用且每处组合都不同导致大量重复实例化。解决方案有三个把装饰器模板中逻辑相同且不依赖具体类型的部分提取成非模板基类或自由函数减少生成的代码量。使用extern template显式实例化某些常用组合避免在多个编译单元重复实例化。如果代码膨胀不可控干脆退回std::function版本让所有装饰器共享同一个函数类型运行时再组装。另外建议在编译时打开-fltoLTO优化链接器会移除部分重复代码。但注意LTO会增加编译时间大型项目需要权衡。4.3 函数装饰器与异常安全用std::function装饰器时有个容易忽略的问题装饰器内部的inner(amount)可能抛出异常。比如日志装饰器里你已经在cout中打印了“before”如果inner抛异常那“after”的日志就不会打印。更糟糕的是如果装饰器捕获了异常准备记录但记录的代码本身也可能抛异常比如磁盘满了会导致原始异常被掩盖。处理思路PayFunc add_logging_safe(PayFunc inner) { return [inner std::move(inner)](double amount) - bool { std::cout [LOG] before std::endl; try { bool result inner(amount); std::cout [LOG] after success std::endl; return result; } catch (...) { std::cout [LOG] after exception std::endl; throw; // 重新抛出原始异常 } }; }异常安全的原则有两层第一层是保证装饰器在异常发生时自身的一致性不要泄漏资源、不要留下未完成状态第二层是不要“吞掉”异常除非装饰器的职责就是处理异常并返回失败结果。重试装饰器就属于后者——它的职责是捕获异常并重试但要注意只捕获特定的异常类型比如网络超时而不是所有异常比如逻辑错误重试也没用。4.4 与代理模式、责任链模式混淆很多C初学者会把装饰器变体和代理模式、责任链模式混为一谈。这三个模式在“包装一个对象/函数”的外在形式上确实很像但意图完全不同。代理模式Proxy强调的是“控制访问”比如延迟加载、访问权限控制。代理和被代理对象通常不完全一致代理可能只暴露部分接口而且代理可能在没有真实对象的情况下存在。装饰器强调“增强功能”它必须保持原有接口不变且在原有逻辑上叠加新行为。责任链模式Chain of Responsibility强调“请求沿着链传递每个节点能处理或跳过”。装饰器链则必须层层调用不能跳过。一个典型的分辨方法是如果某个节点可以选择“不处理并直接返回”那是责任链如果每个节点都必须先处理后传递给下一个那是装饰器。基于std::function的装饰器很容易不小心写成责任链因为每个装饰器都可以在调用inner之前判断条件并提前返回这时候它实际上就变成了“过滤器/中间件”。不要紧这恰恰是C变体的灵活之处——只要你清楚自己到底在实现哪个模式命名上保持一致就不会误导维护者。我在业务代码里遇到过一个被称为“decorator”的类实际逻辑是如果参数非法就直接返回错误根本不会调用inner。这其实是校验代理。把名字改掉之后团队理解成本立刻降了下来。5. 我的经验心得与进一步扩展5.1 什么时候该用装饰器变体什么时候不该用踩过这么多坑之后我对装饰器变体的态度变得很务实不要在代码里为了模式而模式。如果你的设计目标只是“在某个操作前后加点日志”直接写两行代码就够了没必要套装饰器。真正的装饰器需求通常有几个信号同一个基础行为需要多种可选增强且组合方式较多。这些增强需要在多处复用而不是只服务一两个调用点。你希望调用方只依赖原始接口不感知增强的存在。你想把横切关注点日志、安全、重试、缓存从业务逻辑中剥离出来。反过来如果只是单一一处需要加日志或者你的“装饰器”只有一个固定子类那用装饰器反而增加复杂度。C的std::function变体尤其适合“函数级别”的横切关注点但它有类型擦除成本模板混入适合“类级别”的静态增强但组合爆炸和编译期绑定是硬伤。没有全能的变体只有当前场景下最合适的那一个。5.2 下一步可以怎么玩AOP式切面装饰最后分享一个我认为很有价值的进阶方向把装饰器变体往AOP面向切面编程的方向扩展。AOP的核心是把“日志、性能、安全”这类横切关注点从业务代码中抽出来用“切面”的方式在编译期或运行期织入主流程。装饰器模式天然就是实现AOP的一种手段尤其在C里我们可以用模板元编程在编译期完成“切面织入”。比如我开发过一个简单的切面装饰器框架核心是一个宏或模板允许你在一个类的方法上标注多个切面template typename T, templatetypename typename... Aspects struct AspectDecorated : AspectsT... { using AspectsT::Aspects...; using AspectsT::operator...; // 这里只是示意 };实际实现要麻烦得多涉及每个切面的before/after顺序、异常处理、参数修改等。一个更接地气的做法是继续用std::function把切面实现成独立的中间件函数通过一个decorator_chain工具把多个函数按顺序组合起来template typename Ret, typename... Args std::functionRet(Args...) compose_decorators( std::functionRet(Args...) core, std::vectorstd::functionstd::functionRet(Args...)(std::functionRet(Args...)) decorators) { for (auto it decorators.rbegin(); it ! decorators.rend(); it) { core (*it)(std::move(core)); } return core; }这样可以从配置文件里读取需要启用哪些切面运行时动态构建装饰链。我拿这套东西给内部工具写过“审计日志 并发限流 超时控制”三件套只改一行配置就能开关某个切面调试和上线都很方便。这算是把装饰器模式变体用到了极致。如果你正处在C设计模式学习的瓶颈期我建议不要死记GoF的类图而是多从“这个模式在C里能映射到哪些语言特性”的角度去思考。装饰器模式就是一个绝佳的切入点它既能映射到虚函数和继承也能映射到模板、lambda和std::function。把每种映射都写一遍、跑一遍、比较一遍你对C的理解会上升一个台阶。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门