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

news/2025/10/2 10:03:52/文章来源:https://www.cnblogs.com/wzzkaifa/p/19123274

精读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.mzph.cn/news/924776.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

*和 内存和地址 实例代码

#include <bits/stdc++.h> using namespace std; int main(){int number=42;int *ptr=&number;//ptr 存储 number 的地址 cout<<"变量值:"<<number<<endl; //42cout&l…

应用安全 --- 安卓加固 之 IPC

应用安全 --- 安卓加固 之 IPC应用安全 --- 安卓加固 之 IPC 会有一个调用者和被调用者,我们frida只能hook其中一个,无法获取全貌### 为什么看不到真实调用者信息?核心原因 :你看到的是一个 Binder IPC跨进程调用…

深入解析:前端开发,iframe 相关经验总结

深入解析:前端开发,iframe 相关经验总结pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "…

事业单位网站建设费入什么科目网站由谁备案

以下是一个使用DefaultsKit库的简单爬虫程序&#xff0c;用于爬取音频。代码中使用了https://www.duoip.cn/get_proxy的API获取代理服务器。 import Foundation import DefaultsKit ​ let url "https://www.douban.com/music" // 目标网站URL let proxyUrl "…

超市如何建立网站免费行情网站

文章目录 摘要Abstract文献阅读题目引言创新点方法利用长短期记忆网络学习时空演化特征构建用于气象辅助信息编码的堆叠自编码器使用多任务学习发现全市通用模式 模型实验数据集评估准则实验结果 深度学习Self-attentionself-Attention由来self-attention原理self attention代码…

寻找东莞微信网站建设三只松鼠软文范例500字

文章目录 0 前言1\. 目标检测概况1.1 什么是目标检测&#xff1f;1.2 发展阶段 2\. 行人检测2.1 行人检测简介2.2 行人检测技术难点2.3 行人检测实现效果2.4 关键代码-训练过程 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 毕业设计…

完整教程:Linux-01_2(vi / vim 编辑器)

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

金华义乌网站建设企业建网站一般要多少钱

背景 在flink中&#xff0c;如果你想要访问记录的处理时间或者事件时间&#xff0c;注册定时器&#xff0c;或者是将记录输出到多个输出流中&#xff0c;你都需要处理函数的帮助&#xff0c;本文就来通过一个例子来讲解下副输出 副输出 本文还是基于streaming-with-flink这本…

全面解析Umi-OCR手写体识别能力:开源OCR的新标杆 - 指南

全面解析Umi-OCR手写体识别能力:开源OCR的新标杆 - 指南2025-10-02 09:46 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important;…

Playwright MCP浏览器自动化详解指南 - 教程

Playwright MCP浏览器自动化详解指南 - 教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", &quo…

Python获取视频文件的各种属性信息

Python获取视频文件的各种属性信息很多时候我们需要获取视频文件的属性信息,这里记录一下几种使用Python获取视频文件的属性信息的方法。 方法一:使用opencv库 需要安装opencv库pip install opencv-python具体代码如…

iis5.1建网站php培训机构企业做网站

炮兵问题的优化&#xff0c;设立逻辑数组 蛮力法设计思想 有策略地穷举 验证 制定穷举策略避免重复 简单来说&#xff0c;就是列举问题所有可能的解&#xff0c;然后去看看是否满足题目要求&#xff0c;是一种逆向解题方式。&#xff08;我也不知道答案是什么&#xff0c;…

网站建设wix帝国cms能做手机网站吗

1、拷贝构造函数 1.1 什么是拷贝构造函数 拷贝构造函数是一种构造函数&#xff0c;它的功能是创建新对象。也就是说对象还没生成&#xff0c;这时利用另一个对象的拷贝来生成新的对象。 class MyDemo { public:// 默认构造函数MyDemo(){}// 拷贝构造函数MyDemo(const MyDemo…

做网站必须认证吗武夷山网站推广服务

为缓解民生山西认证压力&#xff0c;提高认证效率与认证率&#xff0c;山西省社保局推出“老来网”认证手机app&#xff0c;请未认证人员以及民生山西无法认证人员&#xff0c;采用此认证方式。附&#xff1a;民生山西APP认证流程老来网APP具体操作流程如下&#xff1a;一、 老…

负载均衡式的在线OJ项目编写(七) - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

Arduino-Yun-物联网指南-全-

Arduino Yun 物联网指南(全)原文:zh.annas-archive.org/md5/5adfde3977d9c1890a46c10dfc85e6f2 译者:飞龙 协议:CC BY-NC-SA 4.0前言 物联网(IoT)是科技界的一个增长话题,越来越多的使用众筹活动资助的硬件项目…

深入解析:【笔记】在WPF中Binding里的详细功能介绍

深入解析:【笔记】在WPF中Binding里的详细功能介绍2025-10-02 09:32 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; disp…

2025雕塑厂家TOP企业品牌推荐排行榜,婚庆泡沫雕塑,玻璃钢,城市地标不锈钢,校园筑铜,道具,文旅,婚礼堂泡沫,直播间实景泡沫,水泥景观,商业美陈发光雕塑公司推荐!

当前雕塑行业在发展过程中,面临着多重亟待解决的问题。从成本层面来看,部分企业因存在中间商环节,导致产品价格虚高,客户难以获得高性价比的雕塑产品;在定制能力上,一些厂家受限于设备水平和专业团队配置,无法满…