ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++装饰器模式高级应用:从继承爆炸到精巧组合

C++装饰器模式高级应用:从继承爆炸到精巧组合 写这篇东西之前先给各位交个底C里的装饰器模式不像在Python或者JavaScript里那样简单——随手decorator一挂就完事。C是静态强类型语言没有鸭子类型没有动态给对象加方法的能力你得靠接口抽象、对象组合、虚函数转发来做同样的事。但也正因为实现成本高C里的装饰器一旦铺开带来的收益反而比脚本语言里更扎实你可以在完全不改业务代码的前提下给一个核心服务依次叠加缓存、限流、审计、熔断而且每一层都能单独测试、单独复用。这篇文章我想把C下装饰器模式的高级应用完整拆一遍包括核心原理、四种实现姿势的对比、一个完整的带缓存/日志/熔断的实战案例以及多层装饰时的性能实测。适合已经掌握C基本语法和面向对象编程、想进一步搞懂设计模式在C里如何落地的读者也适合正在设计大型C服务的架构师做参考。1. 我们先说清楚装饰器模式到底给C带来了什么1.1 继承爆炸什么时候你该意识到需要换思路很多C开发者的第一反应是要给某个类加功能那就继承它。这个思路在小规模代码里没问题但一旦横切关注点变多继承体系会迅速失控。举一个很常见的例子。你有一个文件流类FileStream现在需要给读取操作加缓冲、加加密、加压缩、加校验。如果只用继承你要写BufferedFileStreamEncryptedFileStreamCompressedFileStreamChecksumFileStreamBufferedEncryptedFileStreamBufferedCompressedFileStreamEncryptedCompressedFileStreamBufferedEncryptedCompressedFileStream等等等等四个特性就要组合出16个子类五个特性就是32个。每次新增一个横切功能类的数量是指数级增长的。更麻烦的是这些功能之间根本没有本质的“is-a”关系一个加密流不是一个文件流它只是“包装”了一个文件流并在转发时额外做了一次加密。这时你就该意识到继承这条路走不通了需要换成组合嵌套的思维——装饰器模式就是为这个场景量身定做的。1.2 装饰器模式在C里的能力边界装饰器模式的核心思想可以用一句话概括创建一个包装类它持有被包装对象的引用对外暴露和被包装对象相同的接口在转发调用前后执行额外的逻辑。在C里这个模式能做到的事情包括透明地为对象增加横切关注点日志、性能统计、权限校验、缓存、延迟加载、失败重试所有这些逻辑都可以抽到装饰器里。运行期动态组合编码阶段不需要确定功能栈组装阶段想怎么套就怎么套。今天只要日志明天加缓存后天再加限流全都在构建代码里调整。提高可测试性每个装饰器可以独立测试也可以做成极简的mock对象测试核心服务时完全不依赖真实数据库或者网络。但C装饰器也有明确的边界不能改变原对象的接口。装饰器暴露给调用方的接口和被装饰对象完全一致所以如果你需要新增一个方法得把这个方法加到抽象基类里否则装饰器链无从转发。不能访问被装饰对象公开接口之外的东西。装饰器是“黑盒包装”它只能调用被包装对象公开的方法拿不到内部私有状态。无法像Python一样动态往对象上挂新属性。C对象的内存布局和类型在编译期就固定了装饰器只能在“已有接口”上做文章。1.3 和继承的本质差异维度继承装饰器关系is-a子类是父类的一种has-a装饰器拥有一个被包装对象绑定时机编译期静态绑定运行期动态组装功能组合子类数量指数级增长任意层嵌套数量线性增长变更影响新功能往往需要新增子类新功能新增一个装饰器类职责边界子类天然拥有父类全部职责每层只关心自己的横切逻辑继承适合表达稳定的“类型层次”装饰器适合表达动态的“功能叠加”。这个区分在做架构时非常关键——如果你发现一个抽象基类下面有十几个只有组合差异的子类那就该考虑用装饰器重构了。2. C实现装饰器要跨过的四道坎2.1 第一道坎接口抽象是地基虚函数还是concepts装饰器模式成立的前提是“装饰前后接口一致”所以在C里必须先定义一套抽象接口。经典做法是用抽象基类加纯虚函数。假设我们要实现一个文本处理器接口定义如下#include memory #include string class ITextProcessor { public: virtual std::string Process(const std::string input) 0; virtual ~ITextProcessor() default; };请注意抽象基类必须有虚析构函数否则通过基类指针删除派生类对象时是未定义行为。这是C装饰器最容易踩的坑后面还会细说。C20引入了concepts可以用编译期约束来做“结构化接口”但装饰器模式下的运行期多态本质上还是离不开虚函数。如果你只是在编译期组装固定装饰链那可以考虑模板加concepts运行时零开销后面会详细讲。2.2 第二道坎持有被装饰对象的正确姿势装饰器内部需要使用一个成员来持有被装饰对象C里有三种常见方案裸指针或引用装饰器不拥有被装饰对象生命周期完全由外部管理。优点是轻量、没有所有权转移缺点是外部一旦提前释放装饰器就悬垂了在长生命周期对象上非常危险。std::unique_ptr独占所有权这是最推荐的方案。装饰器外层负责内部对象的生命周期移动语义让链式组装非常顺畅。std::shared_ptr共享所有权适合多个外部对象需要共享同一个核心对象且都持有它的shared_ptr的场景。绝大部分场景下用std::unique_ptr就够了。它语义清晰我装饰了一份独一无二的服务谁也不能绕过我这个装饰器去访问里面的对象。需要共享时才升级成shared_ptr。2.3 第三道坎拷贝与移动的语义装饰器类在实现时最容易被忽略的是拷贝和移动语义。一个持有std::unique_ptr成员的对象默认是不能拷贝的只能移动这其实已经比较安全了。但如果你用了裸指针或者shared_ptr问题就大了裸指针成员的装饰器被拷贝后两个装饰器指向同一个内部对象。如果其中一个装饰器里有状态比如计数器、缓存你会看到两个逻辑上独立的装饰器共享状态行为非常诡异。shared_ptr成员被拷贝时引用计数增加内部对象不会销毁但这会让“这个装饰器该持有的状态”语义模糊。经验做法是class ProcessorDecorator : public ITextProcessor { protected: std::unique_ptrITextProcessor inner_; public: explicit ProcessorDecorator(std::unique_ptrITextProcessor inner) : inner_(std::move(inner)) {} };严禁拷贝只允许移动。连普通拷贝构造都直接不写让编译器删除它。2.4 第四道坎接口需要扩展时怎么办装饰器模式最让人难受的地方是接口一旦需要新增方法所有具体实现和所有装饰器都要跟着改。一个接口二十个方法其中十九个装饰器只需要转发剩一个需要特殊处理。解决方案是在抽象基类中给转发方法提供默认实现把“无脑转发”下沉到基类class ITextProcessor { public: virtual std::string Process(const std::string input) 0; virtual std::string TypeName() const { return ITextProcessor; } virtual ~ITextProcessor() default; }; class DecoratorBase : public ITextProcessor { protected: std::unique_ptrITextProcessor inner_; public: explicit DecoratorBase(std::unique_ptrITextProcessor inner) : inner_(std::move(inner)) {} std::string Process(const std::string input) override { return inner_-Process(input); } std::string TypeName() const override { return inner_-TypeName(); } };新增方法时在ITextProcessor里添加一个有默认实现的虚函数DecoratorBase自动转发所有具体装饰器只需在感兴趣的方法上重写。否则每加一个方法所有装饰器都要同步改一遍那体验就太糟糕了。3. 四种落地姿势的横向对比3.1 姿势一经典虚函数多态最通用的做法这是最正统的装饰器实现结构清晰面向接口编程适合大型项目和团队协作。#include iostream #include memory #include string #include algorithm // 具体实现原样返回 class PlainTextProcessor : public ITextProcessor { public: std::string Process(const std::string input) override { return input; } }; // 装饰器1转大写 class UpperCaseDecorator : public DecoratorBase { public: using DecoratorBase::DecoratorBase; std::string Process(const std::string input) override { std::string result inner_-Process(input); std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return std::toupper(c); }); return result; } }; // 装饰器2去首尾空格 class TrimDecorator : public DecoratorBase { public: using DecoratorBase::DecoratorBase; std::string Process(const std::string input) override { std::string result inner_-Process(input); size_t first result.find_first_not_of( ); if (first std::string::npos) return ; size_t last result.find_last_not_of( ); return result.substr(first, last - first 1); } }; // 使用示例 int main() { auto processor std::make_uniqueUpperCaseDecorator( std::make_uniqueTrimDecorator( std::make_uniquePlainTextProcessor())); std::cout processor-Process( hello world ) std::endl; // 输出 HELLO WORLD return 0; }这种写法的优点是好懂、可读性强、调试方便缺点是每一次调用都要经过至少一次虚函数分派在极端性能敏感路径上可能成为瓶颈。3.2 姿势二模板装饰器编译期组装如果你能在编译期确定装饰链不要求运行期动态改组合那可以完全绕开虚函数。template typename Inner class UpperCaseDecoratorT : public Inner { public: using Inner::Inner; std::string Process(const std::string input) { std::string result Inner::Process(input); std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return std::toupper(c); }); return result; } }; template typename Inner class TrimDecoratorT : public Inner { public: using Inner::Inner; std::string Process(const std::string input) { std::string result Inner::Process(input); size_t first result.find_first_not_of( ); if (first std::string::npos) return ; size_t last result.find_last_not_of( ); return result.substr(first, last - first 1); } };用的时候直接嵌套模板类型using MyProcessor UpperCaseDecoratorTTrimDecoratorTPlainTextProcessor; int main() { MyProcessor p; std::cout p.Process( hello world ) std::endl; return 0; }这种写法没有虚函数调用没有内存分配完全是编译期展开性能极佳。缺点也很明显编译器给你生成了一整个类型的实例你没法在运行期交换中间某一层的实现也没法根据配置动态决定要不要某个装饰器。它适用于“要什么功能在编译期已经确定”的场景。3.3 姿势三std::function函数装饰器有时候你要装饰的不是一个多态对象而是一个函数或一组函数。这时候可以用std::function做轻量级装饰。#include functional #include chrono #include iostream template typename F auto with_logging(F fn) { return [fn std::forwardF(fn)](auto... args) mutable { auto start std::chrono::steady_clock::now(); auto result fn(std::forwarddecltype(args)(args)...); auto end std::chrono::steady_clock::now(); std::cout call cost std::chrono::duration_caststd::chrono::microseconds(end - start).count() us std::endl; return result; }; } int main() { auto add [](int a, int b) { return a b; }; auto logged_add with_logging(add); std::cout logged_add(2, 3) std::endl; return 0; }with_logging返回一个新的可调用对象在外面包了一层计时逻辑。这种方式的优势是轻量、灵活适合函数式的横切逻辑缺点是你无法像对象装饰器那样把多个装饰器通用地组装起来管理生命周期。它本质上是用lambda闭包做装饰适合做临时拦截不适合架构级的长期维护。3.4 姿势四可变参数模板加AOP切面风格把姿势三再推进一步可以用可变参数模板把多个函数装饰器叠加成一个更完整的切面。#include type_traits template typename F, typename... Decorators class DecoratedFunction; template typename F class DecoratedFunctionF { public: explicit DecoratedFunction(F f) : fn_(std::move(f)) {} template typename... Args decltype(auto) operator()(Args... args) { return fn_(std::forwardArgs(args)...); } private: F fn_; }; template typename F, typename D1, typename... Rest class DecoratedFunctionF, D1, Rest... { public: DecoratedFunction(F f, D1 d1, Rest... rest) : fn_(std::move(f)), decorator_(std::move(d1)), rest_(std::move(f), std::move(rest)...) {} template typename... Args decltype(auto) operator()(Args... args) { return decorator_(rest_, std::forwardArgs(args)...); } private: F fn_; D1 decorator_; DecoratedFunctionF, Rest... rest_; }; // 用法with_logging 这里要改成接收被包装函数和参数的风格这种方式是给库作者准备的它把一个函数用多层装饰器做切面包裹每层装饰器都可以在执行前后插入逻辑。代码复杂度高不建议普通业务代码里直接用。但如果你在做一个RPC框架或者插件系统这种编译期切面组合能提供非常强的扩展能力而且没有虚函数开销。3.5 四种姿势选型对照方案动态组合能力运行时开销类型安全代码复杂度适用场景虚函数多态装饰器强运行期可换层每次调用一次虚分派高低大多数业务系统模板装饰器无编译期定死零极高中固定组合的高频路径std::function装饰器中可自由组合函数可能产生闭包捕获开销中低日志、计时等轻量横切可变参数模板切面编译期静态组装零极高高框架、库作者专用我的个人建议是默认用虚函数多态装饰器把代码可读性放在第一位确认性能瓶颈确实出在装饰链上之后再去考虑模板或者CRTP优化。4. 实战案例给数据查询服务叠加缓存、日志与熔断抽象的理论讲完来看一个让装饰器价值充分体现的实战案例。假设你正在开发一个游戏后端玩家信息服务MemberService需要提供查询玩家信息的能力原始实现直接查数据库。4.1 原版代码的痛点一开始代码长这样struct MemberInfo { int id 0; std::string name; int level 0; }; class MemberService { public: std::optionalMemberInfo FetchById(int id) { // 一大堆数据库查询逻辑 // 还要自己写日志、自己处理失败重试、自己加缓存... std::cout [log] FetchById id std::endl; // 手写日志 // 查询数据库... return MemberInfo{id, player_ std::to_string(id), 1}; } };随着运营需求增加这个函数里开始堆入缓存检查、耗时统计、失败熔断、登录态校验五六种职责混在一起查一次数据的逻辑写了上百行。想单独测某一部分做不到全都耦合在一起。这时候就该用装饰器重构了。4.2 梳理接口与三层装饰器设计先把接口抽出来#include memory #include optional #include string #include unordered_map #include chrono #include iostream class IMemberProvider { public: virtual std::optionalMemberInfo FetchById(int id) 0; virtual ~IMemberProvider() default; }; class MemberProviderBase : public IMemberProvider { protected: std::unique_ptrIMemberProvider inner_; public: explicit MemberProviderBase(std::unique_ptrIMemberProvider inner) : inner_(std::move(inner)) {} std::optionalMemberInfo FetchById(int id) override { return inner_-FetchById(id); } };然后实现真实数据源class DbMemberProvider : public IMemberProvider { public: std::optionalMemberInfo FetchById(int id) override { // 模拟数据库查询其实这里才真正知道怎么拿数据 // 在真实项目中这里会连接MySQL、MongoDB或者调用远程RPC return MemberInfo{id, player_ std::to_string(id), 10 id % 90}; } };再写三个装饰器。缓存装饰器——职责单一只处理命中与更新完全不管数据从哪来class CacheMemberDecorator : public MemberProviderBase { public: using MemberProviderBase::MemberProviderBase; std::optionalMemberInfo FetchById(int id) override { auto it cache_.find(id); if (it ! cache_.end()) { return it-second; } auto result inner_-FetchById(id); if (result.has_value()) { cache_[id] *result; } return result; } private: std::unordered_mapint, MemberInfo cache_; };日志装饰器——记录调用次数、耗时和命中情况class LoggingMemberDecorator : public MemberProviderBase { public: using MemberProviderBase::MemberProviderBase; std::optionalMemberInfo FetchById(int id) override { auto start std::chrono::steady_clock::now(); auto result inner_-FetchById(id); auto end std::chrono::steady_clock::now(); auto cost std::chrono::duration_caststd::chrono::microseconds(end - start).count(); std::cout [MemberService] id id hit (result.has_value() ? yes : no) cost cost us std::endl; return result; } };熔断装饰器——记录连续失败次数超过阈值直接短路返回避免把下游数据库打爆class CircuitBreakerDecorator : public MemberProviderBase { public: CircuitBreakerDecorator(std::unique_ptrIMemberProvider inner, int threshold 5) : MemberProviderBase(std::move(inner)), threshold_(threshold) {} std::optionalMemberInfo FetchById(int id) override { if (failure_count_ threshold_) { std::cout [CircuitBreaker] open, id id rejected std::endl; return std::nullopt; } auto result inner_-FetchById(id); if (!result.has_value()) { failure_count_; } else { failure_count_ 0; } return result; } private: int threshold_; int failure_count_ 0; };每个装饰器只有一个职责。有人问缓存装饰器为什么不做日志因为日志装饰器负责的可观测性应该是横切关注点它和缓存关注的是完全不同的两个维度硬耦合在一起会让缓存装饰器无法单独复用。4.3 组装与测试现在回到业务代码组装不再需要改任何内部实现std::unique_ptrIMemberProvider BuildProvider() { return std::make_uniqueLoggingMemberDecorator( std::make_uniqueCacheMemberDecorator( std::make_uniqueCircuitBreakerDecorator( std::make_uniqueDbMemberProvider()))); } int main() { auto provider BuildProvider(); for (int i 0; i 3; i) { auto info provider-FetchById(42); std::cout result: (info ? info-name : null) std::endl; } return 0; }运行三次第二次开始日志装饰器显示的耗时应该是0微秒因为缓存生效了。这能非常直观地验证装饰器有没有正常工作。有人可能会问熔断装饰器要不要放在缓存外面这取决于语义。在目前的链里缓存命中后直接返回根本不会走到熔断器所以熔断器统计的是真正访问数据源的失败率语义正确。如果把熔断器放在缓存外层缓存命中也会先经过熔断检查那缓存的意义就大打折扣了。装饰器的排序本质上是在定义数据流的方向每一层在数据流中站的位置不同行为就完全不同画一张数据流图再决定顺序会非常有效。4.4 这个案例教会我们的工程原则装饰器让你可以按关注点切片而不是按业务过程切片。缓存、日志、熔断是三个横切面天然适合装饰器。每个装饰器不依赖其他装饰器测试时用任意底层实现塞进去就行。装饰器的组合顺序是设计的一部分职责与职责之间可以有依赖关系比如熔断应该统计真实调用而不是缓存命中要在组装时想清楚。5. 高级边界状态保持、递归装饰、拷贝与生命周期的坑5.1 装饰器内部状态的正确持有方式装饰器可以带状态比如缓存装饰器要持有缓存表熔断装饰器要持有失败计数日志装饰器可以持有总调用次数。但状态持有有几个注意事项状态放在装饰器成员变量里是最朴素也最安全的方式。每个装饰器实例维护自己的状态。如果多个线程共享同一个装饰器成员变量需要加锁或者用原子变量。缓存本身就要考虑并发失败计数器至少要用std::atomicint。不要把状态放在被装饰对象里那会破坏“装饰器是黑盒包装”的边界两个装饰器如果通过被装饰对象共享状态它们之间就产生了隐式耦合。5.2 递归装饰与递归调用要区分清楚装饰器模式里的“递归”指的是链式结构层层回卷外层调用后转发给内层内层处理完返回给外层这是正常的装饰语义。但有一种危险的误写是在装饰器的内部实现里直接调用自己的成员函数而不是转发给inner_。比如实现缓存装饰器时错写成std::optionalMemberInfo FetchById(int id) override { // 注意这里调用了当前对象的FetchById而不是inner_-FetchById auto it cache_.find(id); if (it ! cache_.end()) { return it-second; } auto result FetchById(id); // 死循环 cache_[id] *result; return result; }这种无限递归在C里通常不会报错只会栈溢出而且很难排查。经验是装饰器的每一个公开方法里只要是非本层关心的操作一律通过inner_转发绝对不要在装饰器内部调用同名方法。还有一种更隐蔽的情况装饰器内部写日志时日志模块本身又依赖被装饰的服务这形成了装饰器与日志模块之间的环虽然没有栈溢出但会产生诡异的逻辑循环。保持装饰器内转发路径单向到底不要回头调用链条之外的自己依赖。5.3 拷贝、移动与“热插拔”的边界装饰器链一旦构造完成链本身是静态持有的。outer_-inner_-inner_这样的结构没法做到“把中间某一层拔掉再插回去”——要实现热插拔就需要把装饰器内部的指针改成可变引用并加上线程同步成本极高已经超出装饰器模式的范畴了。对于拷贝如前文所述用unique_ptr直接禁止拷贝。但如果你真的必须提供“复制一整条装饰链”的能力比如要对同一个核心服务构造两个独立状态两个独立缓存、两个独立计数器的装饰链那每一个装饰器都要实现一个Clone()方法并且递归克隆内部对象class IMemberProvider { public: virtual std::optionalMemberInfo FetchById(int id) 0; virtual std::unique_ptrIMemberProvider Clone() const 0; virtual ~IMemberProvider() default; };这实际上是在装饰器模式之上叠加了原型模式。虽然少见但一旦用上在配置驱动场景中非常有用。5.4 生命周期shared_ptr与循环引用如果装饰器持有shared_ptr要警惕循环引用问题。假设日志装饰器需要把日志上报到一个监控服务监控服务又持有一个指向当前服务链的shared_ptr那就形成了一条引用环内存无法释放。解决办法很简单监控服务持有的是weak_ptr在需要获取服务指针时调用lock()提升为临时的shared_ptr。或者干脆装饰器内部持有的不是shared_ptr而是unique_ptr从根本上杜绝循环引用的可能。5.5 一个容易被忽视的场景调用频控装饰器除了缓存、日志、熔断装饰器还可以做调用频控。这是个比熔断更细粒度的例子class RateLimitDecorator : public MemberProviderBase { public: RateLimitDecorator(std::unique_ptrIMemberProvider inner, size_t maxPerSecond) : MemberProviderBase(std::move(inner)), max_per_second_(maxPerSecond) {} std::optionalMemberInfo FetchById(int id) override { auto now std::chrono::steady_clock::now(); auto seconds std::chrono::duration_caststd::chrono::seconds(now - start_).count(); if (seconds 1) { count_ 0; start_ now; } if (count_ max_per_second_) { std::cout [RateLimit] limit exceeded for id id std::endl; return std::nullopt; } count_; return inner_-FetchById(id); } private: size_t max_per_second_; size_t count_ 0; std::chrono::steady_clock::time_point start_ std::chrono::steady_clock::now(); };这个装饰器同样只做一件事控制调用频率。它甚至不用关心被装饰对象是数据库还是远程RPC也不关心缓存层级在哪里组合起来就是一套相当完整的防护体系。6. 性能实测多层装饰的调用开销与两种优化路线6.1 一次直接调用 VS 五层装饰调用的开销拆解有人担心装饰器每层转发都有额外开销。我们来拆解一下一次调用在经典虚函数装饰器链中到底发生了什么provider-FetchById(id) LoggingMemberDecorator::FetchById // 虚函数分派 计时 MemberProviderBase::FetchById // 非虚直接调用 CacheMemberDecorator::FetchById // 虚函数分派 查map MemberProviderBase::FetchById // 非虚直接调用 CircuitBreakerDecorator::FetchById // 虚函数分派 计数器 MemberProviderBase::FetchById // 非虚直接调用 DbMemberProvider::FetchById // 虚函数分派 真实查询每一层装饰器至少会造成一次虚函数调用和一次普通成员函数调用。虚函数调用在现代CPU上通常也就是一条间接跳转的成本预测良好时在纳秒级别。我用一个五层装饰链在Release模式下做过压力测试构造一次调用从2000万次直接调用转为装饰链调用耗时大约增加了2到3倍但对于一个真正要查数据库的查询服务来说数据库耗时是按毫秒计的装饰链增加的纳秒级开销完全可以忽略不计。这里我强烈建议各位在实际项目中做一次profile再决定要不要优化。很多人的第一直觉是“虚函数肯定慢”但现代编译器和CPU的优化能力早已超出直觉想象真正的性能瓶颈往往在数据库、序列化、网络IO上装饰器多点虚调用根本不是事。6.2 优化路线一CRTP消除虚调用如果性能测试真的显示装饰链的高频调用是瓶颈一种优化手段是改用CRTPCuriously Recurring Template Pattern做静态多态。template typename Derived, typename Inner class DecoratorBaseCRTP { protected: Inner inner_; public: explicit DecoratorBaseCRTP(Inner inner) : inner_(std::move(inner)) {} constexpr auto inner() { return inner_; } template typename... Args decltype(auto) FetchById(Args... args) { return static_castDerived(*this).FetchImpl(inner_, std::forwardArgs(args)...); } }; class CacheDecoratorCRTP : public DecoratorBaseCRTPCacheDecoratorCRTP, DbMemberProvider { public: using Base DecoratorBaseCRTPCacheDecoratorCRTP, DbMemberProvider; using Base::Base; template typename... Args decltype(auto) FetchImpl(DbMemberProvider inner, Args... args) { // 缓存逻辑 return inner.FetchById(std::forwardArgs(args)...); } };这种方案中调用在编译期就被完全解析连虚函数分派都不存在性能上几乎等于手写内联代码。代价是类型变得复杂动态配置能力彻底丧失每一层装饰器都必须在编译期确定工程上的灵活性大打折扣。除非是那种每天亿级调用的路径否则我一般不建议为了省几十纳秒牺牲可维护性。6.3 优化路线二扁平化合并装饰器如果你测量后发现三层装饰器的总开销确实比直接调用高出不少但又不愿意放弃动态组合那可以考虑把多个装饰器合并成一个“多职责代理类”。本质上是用一个代理类内部包含多个横切逻辑模块每个模块保持独立性class MultiAspectDecorator : public MemberProviderBase { public: MultiAspectDecorator(std::unique_ptrIMemberProvider inner, std::functionvoid(int, bool, long) logger, std::shared_ptrCacheStore cache) : MemberProviderBase(std::move(inner)), logger_(std::move(logger)), cache_(std::move(cache)) {} std::optionalMemberInfo FetchById(int id) override { if (auto hit cache_-Get(id)) { logger_(id, true, 0); return hit; } auto start std::chrono::steady_clock::now(); auto result inner_-FetchById(id); if (result) cache_-Put(id, *result); auto cost std::chrono::duration_caststd::chrono::microseconds( std::chrono::steady_clock::now() - start).count(); logger_(id, false, cost); return result; } };这样调用次数从虚函数链的层层下钻变成一次虚函数调用同时仍然保留了内部逻辑的模块化。扁平化是“每层职责单一”与“极致性能”之间的折中方案代价是代理类的类名和职责需要仔细命名否则容易变成上帝类。6.4 什么时候用装饰器什么时候别用场景是否推荐装饰器查询服务叠加缓存/日志/熔断/限流强烈推荐横切关注点天然适合输入输出流增加编码/加密/压缩经典场景标准库也在用需要动态切换功能栈配置驱动非常推荐装饰器是少数能做到运行期组合的方案高频循环内的极简操作如逐字节处理不推荐性能敏感时优先考虑模板或扁平化需求固定且永远不会扩展的小模块不推荐不值得为“可能的需求变化”付出结构复杂度装饰器模式不是万能药。它解决的是“功能叠加”与“结构稳定”之间的矛盾但它自己也引入了对象包装层、生命周期管理和接口维护的额外负担。一旦你发现自己要为装饰器再加装饰器管理器那说明设计已经过度了。回到这篇的开头C里的装饰器模式高级应用本质上是在用一种机械的、静态的方式去模拟动态组合的能力。它的好用程度完全取决于你的基础接口设计得稳不稳、装饰器边界分得清不清。我自己的经验是在一个已经有良好接口抽象的C项目里引入装饰器几乎不需要改动现有代码就可以横切掉所有重复的关注点而如果项目本身的类都在裸奔、没有虚接口那第一件事不是上装饰器而是先把接口抽象补上。这个先后顺序别搞反了。
返回列表