当前位置: 首页 > news >正文

精读C++20设计模式:结构型设计模式:装饰器模式

精读C++20设计模式:结构型设计模式:装饰器模式

前言

​ 扩展!这就是装饰器模式的功能!就像一棵圣诞树,你装饰了它,你就会得到一颗装饰后的圣诞树!它具备更好的观赏功能了!同时,他还具备一般圣诞树一样的功能。这种设计模式就是装饰器模式。

​ 或者说——当我们想要更加有机(自由的?动态/静态都支持的?正交组合的?好像都对!)的扩展我们的功能而不去修改已有的代码。那么这个时候装饰器模式就是合适的!

什么是装饰器模式

​ 举个例子,我们现在正在扩展Shape的子类,假设我们后面要派生具备非常非常复杂的Shape,举个例子:复杂的若干基本图形组合出来的复杂图形,这个时候我们发现,如果我们要硬派生出来一个Shape属于是有点地狱了,您看:

struct Shape {virtual string() const = 0;
};struct ComplexCompoundShape {void resize() ...;void string() override ...;void color() ....;void drawMe() ...;
};

​ 为此,我们发现,很多情况下他们是一些类的正交组合。我们完全可以将他们由一系列基本的Class组合出来一个复杂对象,而不是硬耦合进去。装饰器就是这样思考后规范化的结构。而且,他提出了两种重要的组合方式:

  • 动态的组合:我们可以动态的组合一些对象,而不是在编译器就决定好了我们的对象是什么样的:或者说,他们根据系统的状态可以产生不同组合的对象(注意,这个不是同一个对象的组合可以改变行为)
  • 静态的组合:我们可以静态的组合一些对象,你马上就能想到模板来做这个事情。对!我们稍后会介绍
  • 运行时的组合:笔者自己写代码的时候,比如说手搓日志框架的时候,需要动态的对全局的日志系统热插拔装饰器,这个时候咱们可以将装饰器本身作为一个构造参数传递进去,构成由装饰器组合成的链条延申进行装饰。

动态的组合

struct ShapeDecorator : Shape {
protected:std::shared_ptr<Shape> inner_;
public:ShapeDecorator(std::shared_ptr<Shape> inner) : inner_(std::move(inner)) {}void draw() const override { if(inner_) inner_->draw(); }std::string describe() const override {return inner_ ? inner_->describe() : std::string{};}
};

​ 看起来有趣很多了!我们这样扩展了我们的Shape:实现上,我们引入一个 ShapeDecorator,它持有一个 shared_ptr<Shape>,并负责把 draw()describe() 转发给内部对象。具体的装饰器(比如 RedBorderScaled)只需继承这个基类并在合适时机插入自己的逻辑。现在,我们尽管不知道inner到底做了啥?但是我们可以保证,不管inner如何,他现在都具备了draw的能力和describe的能力。

​ 我们甚至可以按照一种奇妙的方式包起来产生一个链表!

​ 比如说,我们可以把 Scaled 包在 Circle 外面,然后再把 RedBorder 包在 Scaled 外面,运行时产生的是一条链:外层负责边框,内层负责缩放,最里层负责绘制圆形。这样做的好处在于组合是动态的——你可以在运行时任意改变包装顺序、生成不同的外观,而不必在编译期穷举所有组合。

​ 代价则是每层都增加一次间接调用和一个对象实例,如果频繁创建/销毁或在性能敏感的热路径,会产生可观的开销。此外,若装饰器需要传递或拦截诸如 resize() 之类的状态修改接口,你必须在每层显式转发或实现拦截逻辑,否则状态可能只改变在内层却未被上层感知。

​ 在有些场合你并不需要共享底层对象,这时可以把 shared_ptr 换成 unique_ptr,以更清晰地表达所有权。如果你又需要在多线程环境热替换整条装饰链,可以把根引用放到 std::atomic<std::shared_ptr<Shape>>,新链构造完成后用原子 store 一下替换过去,这样正在使用旧链的线程能安全完成当前调用,而后续调用会看到新链,实现近乎无缝的热插拔。

静态组合:模板化装饰器 / Policy(编译期组合、零开销)

​ 当你清楚地知道组合在编译期就不会改变、且对性能极为敏感时,咱们完全可以把事情搞的简单一些,那就是使用模板装饰器!出于上述考虑,模板混入(mixin / policy)会非常吸引人。这里的思想是把装饰器从“运行时对象”变成“编译期的类型包装”。最简单的版本是让装饰器通过模板继承一个 Base 类型,直接把行为内联到最终类型里。

// 基本形状(非多态)
struct CircleRaw {void draw() const { /* 绘制 Circle */ }std::string describe() const { return "Circle"; }
};// 模板装饰器:继承并扩展 Base
template<typename Base>
struct ScaledMixin : Base {double factor_;template<typename... Args>ScaledMixin(double f, Args&&... args) : Base(std::forward<Args>(args)...), factor_(f) {}void draw() const {// 先缩放,再调用 Base::draw()Base::draw();}std::string describe() const {return Base::describe() + " + Scaled(" + std::to_string(factor_) + ")";}
};

​ 嘿!你看到了嘛?现在我们利用静态的方式,扩展了Circle,具备了可画的功能!

​ 我们完全可以像搭积木那样把多个 mixin 嵌套起来:RedBorderMixin<ScaledMixin<CircleRaw>>,编译器会在生成代码时把这些层次内联、优化掉多余开销,得到接近手写内联函数的效率。问题在于类型会爆炸:每一种组合都是新类型,不能把这些不同组合的对象放进同一个 std::vector(除非再做 type-erasure 或写适配器)。另外,构造参数转发在多层混入时需要谨慎设计,否则构造器参数会变得难以管理;常见的做法是约定每种 mixin 的构造参数顺序或使用 tag-based 构造器以确保传参不会混乱。

​ 如果你既想要静态组合的性能,又希望外部代码通过统一接口使用这些类型,可以为静态组合写一个薄适配器,把静态类型包到实现 Shape 抽象的 wrapper 里(StaticAdapter<T> : Shape),这样内部实现是静态高效的,外部仍然可以通过指向 Shape 的多态指针来管理。

template<typename T>
struct StaticAdapter : Shape {T impl_;template<typename... Args>StaticAdapter(Args&&... args) : impl_(std::forward<Args>(args)...) {}void draw() const override { impl_.draw(); }std::string describe() const override { return impl_.describe(); }
};

这个折衷在许多性能关键但接口统一的系统里非常实用:平时内联执行,偶尔通过多态暴露给插件或配置层。

运行时可配置与热插拔:把装饰器当作可注册组件

到了工程化阶段,你可能希望把装饰器变成可配置、可插件化的单元。想象你的系统读取一个 JSON 配置:["scaled:1.5", "red_border"],然后在运行时构造出恰好符合配置的装饰链。这时,我们要把每个装饰器的“制造方法”注册到一个工厂表中:每个工厂接收一个 ShapePtr(当前链的内层)并返回包裹后的 ShapePtr。装饰器的注册既可以在静态初始化时完成,也可以在插件加载时动态注册。基于这种注册机制,构建函数会从配置的尾部往前构造,这样最内层是基础对象,外层依次套上装饰器。

using ShapePtr = std::shared_ptr<Shape>;
using DecoratorFactory = std::function<ShapePtr(ShapePtr)>;static std::unordered_map<std::string, DecoratorFactory>& registry() {static std::unordered_map<std::string, DecoratorFactory> r;return r;
}void register_decorator(const std::string& name, DecoratorFactory f) {registry()[name] = std::move(f);
}

​ 这种方式是咱们的老朋友了,工厂模式中我们已经这样做过!

为了保证运行时的可替换性和线程安全,切换链的代码需要考虑并发访问。常见策略是把当前根对象放在 std::atomic<std::shared_ptr<Shape>> 中,重加载配置时构造新链并做一次原子交换。使用这种方式,替换操作非常快且几乎不会阻塞正在执行的线程,旧链会在引用计数归零后自动销毁。

std::atomic<ShapePtr> root;void reload_config_and_apply(const std::vector<std::string>& cfg) {auto base = std::make_shared<ConcreteRootShape>();auto new_root = build_from_config(base, cfg);root.store(new_root); // 原子替换,后续访问看到新链
}

​ 需要注意的是,如果装饰器含有可变共享状态(比如缓存、计数器),热插拔可能会引发不一致或 race 条件,因此要么保证这些状态是线程安全的、要么设计为无状态或把状态局部化。

​ 另一个更“函数式”的实现路径是把绘制行为抽象成 std::function<void()>,每一个装饰器接收一个先前的 DrawFn 并返回一个新 DrawFn

using DrawFn = std::function<void()>;
DrawFn base_draw = [](){ /* draw base */ };
DrawFn with_red = [prev = base_draw]() { prev(); /* draw border */ };

​ 这种方式非常适合只需要单一行为的情况(比如日志输出的包装链),它的优势是组合直观、配置驱动简单,但缺点是难以在函数中保存复杂状态或同时支持 describe()resize() 这类额外 API,通常需要配合结构体保存更多元信息。

折衷与实战经验(什么时候采用哪种风格)

在真实工程里,很少有“只有一种正确答案”的情况。若你正在写 UI 的绘制路径、且需要尽可能减少每帧的开销,那么静态混入(编译期组合)会让你的内联化和优化空间更大;如果你做的是一套服务端的日志框架或工具链,需要在运行时通过配置或插件改变行为,那么动态装饰器配合工厂注册则提供了必要的灵活性。更常见的做法是混合:把常见的组合以静态方式实现并对外暴露适配器,使其能以多态方式参与运行时链;把那些真正需要热插拔、按需加载的功能做成工厂化插件,这样既保留了性能优势,又得到了运行时可配置性。

在实现细节上要格外警惕几个“坑”:装饰器链会改变对象标识(外层对象并不是内层对象),对等价性或序列化需求要预先设计好 unwrap() 或 ID;在使用 shared_ptr 链时小心循环引用;如果你希望序列化和反序列化链结构,把链的“配置字符串/JSON”存起来通常比直接序列化对象更可靠。并发环境下替换链需要用原子交换等技巧,且确保装饰器内部状态是线程安全或不可变的。

总结

我们遇到的问题

在系统中功能点呈正交组合而不是线性继承时,单纯通过继承会导致类数量爆炸、耦合度过高和维护困难。举例来说,图形对象可能同时需要颜色、边框、缩放、阴影等多种可复用特性,如果把每一种组合都写成独立子类,代码会迅速变成不可控的“组合地狱”;另外,有些场景还要求运行时能按配置或插件动态改变行为,这使得静态继承进一步显得不灵活。并且在性能敏感路径上,频繁的虚调用和对象包裹又会带来额外成本。总体上,问题是如何以最小的修改量、良好的可复用性和可维护性,同时满足运行时/编译期的性能与灵活性需求。

装饰器模式提供的解决思路

装饰器模式提供的解决思路是把额外行为从类继承中抽离出来,封装为可叠加的“包装”或“混入”单元,并在需要的时候把这些单元以链式或嵌套的方式组合到基础对象上。这样,功能变成可重用的模块,既可以在运行时通过对象包装(或工厂链)灵活组合,也可以在编译期通过模板混入实现零开销内联。装饰器的关键在于把扩展行为作为独立的层(或类型)插入,而不是把所有可能的组合写成一堆具体子类;同时可以通过适配器、工厂注册或原子替换等手段,把静态和动态方案结合起来以兼顾性能、可配置性与热替换能力。

方案对比
方案优点缺点
动态组合(运行时装饰器)支持运行时任意顺序组合与即时扩展,接口统一,便于插件化与按需包装;实现直观,适合需要动态行为的场景。每层引入虚调用和指针开销,链过长时调试与序列化复杂;若有状态修改接口需要显式转发;需注意生命周期与循环引用。
静态组合(模板混入 / Policy)编译期生成代码,能内联优化到接近零开销,适合性能敏感路径;类型系统在编译时捕捉错误。每种组合是不同类型导致类型膨胀,构造参数转发复杂,不能在运行时切换或放入统一容器(需额外适配器或 type-erasure)。
运行时可配置 / 热插拔(工厂 + 注册)能根据配置/插件在运行时构造并热替换装饰链,部署灵活,适合需要热更新或配置驱动的系统;便于模块化加载。依赖工厂与解析逻辑,运行时仍有间接调用开销;实现更复杂(线程安全、参数解析、序列化);装饰器内部状态需妥善并发控制。
http://www.dtcms.com/a/422887.html

相关文章:

  • (数据结构)链表OJ——刷题练习
  • 怎么做网站源码温州建网站
  • 云服务器做淘客网站苏州网站制作及推广
  • hive启动报错
  • (基于江协科技)51单片机入门:6.串口
  • UE5 小知识点 —— 09 - 旋转小问题
  • Git 暂存文件警告信息:warning: LF will be replaced by CRLF in XXX.java.
  • 石狮网站建设价格万网网站根目录
  • VBA ADO使用EXCEL 8.0驱动读取 .xlsx 格式表格数据-有限支持
  • freeswitch集成离线语音识别funasr
  • 建设网站管理规定源码做网站图文教程
  • Qt 入门:构建跨平台 GUI 应用的强大框架
  • Spring WebFlux调用生成式AI提供的stream流式接口,实现返回实时对话
  • 【学习笔记】高质量数据集
  • 微美全息科学院(WIMI.US):互信息赋能运动想象脑电分类,脑机接口精度迎来突破!
  • 协议 NTP UDP 获取实时网络时间
  • 公司网站可以分两个域名做吗残疾人网站服务平台
  • spark pipeline 转换n个字段,如何对某个字段反向转换
  • 学习React-18-useCallBack
  • 长沙制作网站的公司与传统市场营销的区别与联系有哪些
  • 从语言到向量:自然语言处理核心转换技术的深度拆解与工程实践导论(自然语言处理入门必读)
  • 无人设备遥控器之无线发射接收技术篇
  • 《从数组到动态顺序表:数据结构与算法如何优化内存管理?》
  • 浏览器正能量网站2021网页设计免费模板图片
  • 花生壳内网穿透网站如何做seo优化目前最好的找工作平台
  • 1-wireshark网络安全分析——VLAN基础细节详解
  • android studio 无法运行java main()
  • 如何用 Claude Code 搭建安全、可测、可自动化的 GitHub CI 流程?
  • K6的CI/CD集成在云原生应用的性能测试应用
  • Selective Kernel Networks 学习笔记