目录标题
- 第一章: 设计模式与 C++:初识与动机
- 第二章: 创建型设计模式
- 第三章: 结构型设计模式
- 第四章: 行为型设计模式
- 第五章: C++ 设计模式的综合思考与实用总结
- 结语
第一章: 设计模式与 C++:初识与动机
在软件开发的领域里,“设计模式”被视为一套久经考验的解决方案,它从架构层面抽象并总结了常见的问题和应对策略。对于 C++ 这门既能接近硬件、又能进行高层抽象的语言来说,设计模式更像是一座桥梁,一端连接着底层的内存与性能控制,一端通往更灵活、更易维护的代码结构。正如心理学家马斯洛曾提到“当手里只有一把锤子时,看到的就全是钉子”,在 C++ 里若只靠 “new / delete” 处理对象,势必会在复杂项目中陷入困境,而设计模式则为我们提供更多手段与思维。
1.1 为什么要学习设计模式
1.1.1 面向扩展与维护的必然需求
C++ 项目往往具有迭代周期长、代码库大、需求变化频繁的特点。若只凭借语言的基本特性,很容易在后期演进中出现逻辑分散、耦合加剧的问题。此时,拥有一套“随时可以拿来借鉴”的设计模式,就能帮助团队维持架构韧性,并减少大规模重构带来的不确定性。
1.1.2 提升沟通效率与团队协作
设计模式提供了统一的话语体系,使得开发者能更精准地描述需求或实现方案。从“小团队内部”到“跨团队合作”,使用“抽象工厂”“观察者”等名词就能迅速达成对接方案。对资深工程师而言,设计模式更能帮助他们在代码评审和架构讨论中快速指出潜在问题或提出改进思路。
1.2 C++ 设计模式的核心特征
1.2.1 灵活运用 RAII 与多态
C++ 相较于其他语言,拥有更丰富的资源管理与多态机制:
- RAII:借助智能指针、构造析构函数来自动管理对象生命周期,巧妙地避免内存泄漏与资源混乱。
- 多态:绝大部分设计模式都依赖基类指针或引用来对接不同派生类,实现对接口编程,减少对具体实现的依赖。
1.2.2 编译期与运行期并行的优势
除了“经典的虚函数多态”,C++ 还能通过模板(编译期多态)来减少运行时开销。尤其在一些性能敏感场景下(如图形渲染、网络处理),可以使用“策略模式 + 模板” 来在编译期决定算法组合,兼顾可维护性与高性能。
1.3 学习与使用设计模式的关键心态
1.3.1 不为模式而模式
设计模式不是花哨的装饰,而是解决实际问题的武器。若项目规模尚小、需求单一,可以先做简单封装,不必一上来就用繁杂的模式“武装到牙齿”。在演进中,当你发现系统某些部分开始臃肿、修改牵扯面广时,再将设计模式的思想恰当地渗透进去,往往能事半功倍。
1.3.2 持续反思与演进
在软件工程的世界中,每一个模式都不是一劳永逸的。“我们最终都要返回真实的实践之中”,某位哲学家曾言及此意:任何模式都需要在项目环境下不停迭代、微调,形成适合自己团队的最佳实现。比如单例模式被公认为易滥用、难测试,却仍在某些场景下(如日志管理、全局配置)大放异彩——关键在于把握场合、限制依赖、预留可替换接口。
1.4 全书结构与阅读指引
- 第二章:创建型模式
探讨如何在 C++ 中更好地封装对象的创建过程,并从首选方案、无奈之举、关键原则三个维度深度解读常见的创建型模式(工厂方法、抽象工厂、建造者、原型、单例)。 - 第三章:结构型模式
分析如何组织类与对象之间的依赖关系,让代码更易拓展、更易复用。这些模式包括适配器、桥接、组合、装饰器、外观、享元、代理等。 - 第四章:行为型模式
重点关注对象间的职责分配与协作方式,从责任链、命令、观察者到状态、策略、访问者等,让你对系统的行为管理更加游刃有余。 - 第五章:综合思考与实用总结
结合 C++ 语言特性,从项目落地的角度出发,分享在真实开发中如何更好地运用、组合和演进这些设计模式,并探讨测试、性能、持续集成等方面的常见问题和应对策略。
本章从C++ 与设计模式的关联、核心特征以及学习心态出发,为后续章节奠定了整体基调。在下一章,我们将正式进入创建型模式的世界,看看它们如何在 C++ 中构建灵活而安全的“对象生产线”。愿你能怀着务实与探索的精神,循序渐进地掌握这些模式,使它们真正成为你在项目开发中可随时依赖的编程思维与技能工具。
第二章: 创建型设计模式
本章将基于三段式结构,对Factory Method(工厂方法)、Abstract Factory(抽象工厂)、Builder(建造者)、**Prototype(原型)以及Singleton(单例)**五种创建型设计模式进行探讨。
正如一位心理学家所言,“我们无法选择外在环境,但我们可以选择如何创造性地适应”,希望通过以下内容,让你在 C++ 项目中更灵活、从容地面对各种对象创建的挑战。
2.1 Factory Method 工厂方法
2.1.1 首选方案
- 用多态 + 虚函数隔离创建逻辑
在 C++ 中,通过基类的纯虚函数createProduct()
,让不同子类(具体工厂)分别实现具体产品对象的创建。客户端只需依赖抽象工厂指针或引用,就能透明地获取不同产品。 - 结合智能指针,避免内存泄漏
工厂方法常返回std::unique_ptr<Product>
或std::shared_ptr<Product>
,这样当产品对象不再使用时,会自动释放资源。
2.1.2 无奈之举
- 在一个大函数里写多重 if-else
历史原因或时间仓促时,可能只能在某处用switch
/if-else
来根据类型判断并new
不同对象。- 为减少影响,尽量将这段 “大 if-else” 收敛到唯一的静态工厂函数或独立名字空间。
- 后续若要扩展,可以把这些分支拆分成具体工厂子类,逐步过渡到真正的工厂方法。
2.1.3 关键原则
- 抽象优先:将可变的创建逻辑放到子类,保持主干(基类)稳定。
- 接口隔离:客户端只知道“我拿到一个
Factory
”,不关心内部如何决定new
哪种产品。 - 测试便利:在测试环境下,可以用 Mock 的工厂子类来替换真实工厂,测试逻辑变得可控。
2.2 Abstract Factory 抽象工厂
2.2.1 首选方案
- 面向“产品族”编程
当系统需要同时生成一组相互依赖或统一风格的产品(如跨平台 UI 控件),定义一个抽象工厂接口,内部含多种createXxx()
函数,用于创建系列产品(Button
,TextBox
,ScrollBar
等)。 - 多层抽象与工厂方法结合
抽象工厂的内部也常用工厂方法来真正执行对象创建,减少重复代码,让产品族内部的逻辑更清晰。
2.2.2 无奈之举
- 集中式 if-else 同时创建多种产品
若团队暂时无法接受接口层级膨胀,可能在一个函数中写if (platform == Windows) { new WindowsButton; new WindowsScrollBar; } else { ... }
。- 也要尽量只在一处写这些判断,勿在各处散落不同平台的分支,以免后期维护困难。
- 日后如果要增加新的平台(如 macOS),可以再将该逻辑拆分为子类工厂,顺势升级为抽象工厂模式。
2.2.3 关键原则
- 同一产品族内部的一致性:抽象工厂确保同时得到的一组对象能正常协作,避免不兼容的组合。
- 接口要“瘦身”:别滥加产品创建函数,若产品族过大,可酌情拆分多个抽象工厂接口。
- 扩展与维护成本:新增一个产品类型时,需要在所有工厂子类中添加对应的创建接口,要评估人力与风险。
2.3 Builder 建造者
2.3.1 首选方案
- 分步骤构建复杂对象
将对象的创建分解为多个步骤(buildPartA()
,buildPartB()
, …),由 Director 统一调度 Builder 的这些步骤,最终拿到建造完成的对象。 - 多 Builder 并行扩展
根据不同需求,可以有不同的 Builder 子类,各自实现不同的构建细节,但相同的构建流程由 Director 调用。
2.3.2 无奈之举
- 巨大的构造函数或“静态工厂函数”
如果仅需要稍微简化一下参数传递,开发者有时会把所有初始化逻辑都放进一个“超大构造函数”或“静态工厂函数”里。- 暂时性方案:至少将其封装在一个函数中,不在业务代码中散落对对象内部的复杂操作;
- 后续再拆分为多个“子步骤”,一步步升级到 Builder 模式。
2.3.3 关键原则
- 单一职责:Builder 只负责如何做“部件装配”,Director 负责 “装配次序”,二者分工明确。
- 线程安全与资源管理:C++ 中构建过程可能涉及内存分配、文件句柄等,应使用智能指针并注意异常安全。
- 别为模式而模式:若对象并不复杂或变化不大,也可不必动用 Builder 的整套仪式,用简单的辅助函数即可。
2.4 Prototype 原型
2.4.1 首选方案
- 克隆已有对象
当对象初始化昂贵或其状态复杂时,先创建一个“原型对象”,然后通过clone()
快速复制,以生成新对象。 - 深拷贝 vs 浅拷贝
在实现clone()
时,根据业务需求选择深拷贝(复制全部资源)或浅拷贝(共享部分资源),并且要在类层次中使用虚函数确保派生类正确克隆。
2.4.2 无奈之举
- 手写拷贝构造 / assignment
有时在类中直接写一个自定义拷贝构造或复制赋值即可,但若项目里出现多态,则需格外小心,避免“对象切片”或“派生成员未被复制”。 - 资源管理麻烦
如果项目中使用原生指针管理资源,就需要手动处理复制时的分配与释放,极易出错。
2.4.3 关键原则
- 正确的多态克隆:基类
clone()
通常是纯虚函数,派生类实现时必须return std::make_unique<Derived>(*this);
或类似操作。 - 避免重复开销:若对象中含较大数据块或昂贵资源,确定是否可以用浅拷贝 + 引用计数减少内存占用。
- 审视原型需求:并非所有初始化昂贵的对象都适合 Prototype,评估后再做取舍。
2.5 Singleton 单例
2.5.1 首选方案
- 依赖注入 + 接口抽象
即使需要单例,也最好先设计一个抽象接口,通过依赖注入方式让大部分逻辑依赖接口而非单例本身,方便测试或替换实现。 - 懒加载 + 线程安全
C++11 后可用“函数内静态变量”实现线程安全懒加载,示例:static Singleton& instance() { static Singleton s; return s; }
2.5.2 无奈之举
- 直接到处
Singleton::instance()
若历史代码全是这种直接调用,难以改动,可先在外层封一层“全局函数”并开放“setter”来替换,或者提供一个工厂接口做伪装。 - 全局变量
有时团队为了方便调试或快速开发,会把全局变量当成单例用,但要面临生命周期管理的各种潜在问题。
2.5.3 关键原则
- 减少滥用:单例是全局共享状态的代名词,过度使用会导致耦合爆炸、测试困难。
- 封装访问点:只在有限的地方调用单例,最好搭配接口或工厂函数,以免散布到全项目。
- 资源销毁与重入:某些场景下需要考虑单例对象的销毁时机,以及是否允许多次初始化。
2.6 多角度比较与小结
下面通过一张对比表,从使用场景、实现难度、测试难度、易错点四个角度,来整体回顾五大创建型模式:
模式 | 使用场景 | 实现难度 | 测试难度 | 易错点/陷阱 |
---|---|---|---|---|
Factory Method | 需根据运行时条件选择不同产品,但产品彼此互斥 | ★★ | ★★ | 子类较多会导致管理复杂 |
Abstract Factory | 一次创建多个相关产品,如跨平台 UI | ★★★ | ★★★ | 新增“产品类型”需要修改所有工厂子类 |
Builder | 构建流程复杂且稳定,但产物形式可变 | ★★ | ★★★ | Director 与 Builder 协作逻辑复杂,需注意异常安全 |
Prototype | 复制对象代价更低,或需保留原对象状态 | ★ | ★ | 多态克隆、资源拷贝,易出现对象切片或重复释放 |
Singleton | 系统只需一个唯一实例(或必须全局共享) | ★ | ★★ | 滥用导致全局耦合;需警惕线程安全与资源销毁 |
“海德格尔曾说,技术的本质并非技术本身,而在于人对自身存在的反思。” 在软件设计中,同样需要反思我们所选择的每一种模式所带来的影响:是让系统更灵活可维护,还是无形之中埋下耦合与膨胀的种子。
2.6.1 重点回顾
-
首选方案:
- Factory Method/Abstract Factory:通过多态隔离复杂创建逻辑,实现对新需求的开放。
- Builder:分解复杂构造步骤,提升可读性与扩展性。
- Prototype:快速复制,减少昂贵初始化。
- Singleton:保留唯一实例,但要谨慎使用。
-
无奈之举:
- 大量的
if-else
/switch
;超大构造函数;“公共全局变量”之类的速成方案。 - 如果必须采用它们,务必封装到最小范围,为后续重构或扩展保留空间。
- 大量的
-
关键原则:
- 封装一处,拒绝散落:将可变的创建逻辑集中在工厂或构建器中,避免到处
new
。 - 考虑测试与可维护性:是否提供注入接口、Mock 工厂、简化依赖?
- 视项目情况而定:设计模式不是银弹,谨防过度设计或滥用带来的反作用。
- 封装一处,拒绝散落:将可变的创建逻辑集中在工厂或构建器中,避免到处
在下一章节中,我们将转向结构型设计模式,重点讨论如何优雅地组织类与对象之间的依赖关系。愿这些“创建型”的思考,能在你的 C++ 代码中落地生根,为项目奠定更稳固的基础。
第三章: 结构型设计模式
在上一章节,我们探讨了创建型设计模式如何帮助我们有序地创建对象。而在本章,我们将聚焦结构型设计模式,这些模式主要关心类与对象之间如何组合,以便在系统中实现更灵活的结构组织。例如,在 C++ 项目中,如何让现有接口与新需求对接?如何减少重复代码?如何屏蔽复杂子系统?这些都属于结构型模式想要解决的问题。
结构型模式主要包括以下七种:适配器(Adapter)、桥接(Bridge)、组合(Composite)、装饰器(Decorator)、外观(Facade)、享元(Flyweight)、以及代理(Proxy)。
本章依旧采用三段式结构,为每个模式提供实战分析与思考。正如一位哲学家所言:“理解结构的内涵,是避免迷失在细节中的关键。”
3.1 Adapter 适配器模式
3.1.1 首选方案
-
包装旧接口以对接新需求
当系统中存在一个无法直接复用的旧接口(或第三方接口),又不想大范围修改旧接口时,编写一个适配器类,将新接口调用转换为旧接口的调用。在 C++ 中常用类适配器(通过继承)或对象适配器(通过组合)实现。 -
最小修改原则
在 C++ 项目里,如果旧接口涉及底层资源(如网络句柄、文件句柄),应将其封装在适配器内部,避免让新系统过度接触底层资源。适配器类只暴露高层方法,最小化新旧接口耦合。
3.1.2 无奈之举
- 直接修改旧接口
如果项目时间紧迫,有时会选择直接改动旧的接口,让其兼容新需求。但这可能牵一发而动全身,引发大范围回归测试或甚至破坏已有功能。- 暂时性做法:若必须修改,也要将新旧接口的差异集中到一个小范围内,减少对全局的影响。
3.1.3 关键原则
- 保持单向依赖:新系统依赖适配器,适配器依赖旧接口,而不是相反。
- 浅封装 vs 深封装:如果旧接口包含过多不必要信息,可在适配器内部进行裁剪或转换。
- 谨慎测试:适配器牵涉“两个世界”——新接口与旧接口,需充分验证适配逻辑与资源管理。
3.2 Bridge 桥接模式
3.2.1 首选方案
- 分离抽象与实现
在 C++ 中,如果一个类(或一组类)同时包含多个变化维度(例如图形形状和渲染平台),可以将“抽象部分(形状)”与“实现部分(渲染)”拆分到不同的继承层次,并在运行时或编译时将它们桥接到一起。 - 接口指针/引用
通过在抽象层持有实现层的指针或引用,动态关联具体实现。这样一来,就能在不修改抽象层的前提下,新增或替换实现层,反之亦然。
3.2.2 无奈之举
- 单一继承层级,功能混在一起
当项目初期规模较小时,可能将“形状逻辑”和“渲染逻辑”写在同一个类层次中。但随着需求复杂度上升,频繁改动会让类爆炸式膨胀。- 若暂时无力重构,可通过命名空间或独立文件做初步隔离,把实现部分抽到专门的区域中,留待后续拆分为正式的桥接结构。
3.2.3 关键原则
- 关注变化维度:桥接模式把两个或更多维度的变化拆分到各自的继承层次;新增维度时不会影响另一条继承线。
- 对象合成优先于类继承:C++ 下常用指针(或智能指针)持有实现对象,而不是多层次多重继承。
- 利于扩展和测试:新增实现类后,无需修改抽象层即可“桥接”新功能,测试时也可用 Mock 实现替换原有逻辑。
3.3 Composite 组合模式
3.3.1 首选方案
- 树形结构、统一对待叶子和容器
当系统需要处理部分-整体的树形层级关系(如文件系统、界面控件树),组合模式允许我们用同一个接口来操作“叶子节点”和“容器节点”。在 C++ 中,可以让“叶子类”和“容器类”都继承自同一个抽象基类Component
,让容器类内部持有一组Component*
。 - 智能指针管理子节点
对象树容易出现生命周期管理的困扰:谁来负责删除子节点?在现代 C++ 中,可使用std::unique_ptr<Component>
储存子节点,以确保自动清理资源。
3.3.2 无奈之举
- 平铺所有对象,不做层级管理
对于非常简单的场景,有时会用一个容器(如std::vector
)直接放所有对象,靠对象属性标记父子关系。虽然这样写起来轻量,但后期操作往往冗长且不清晰。- 后续如出现大规模树形结构,就必须回归到组合模式的思路,统一抽象父子关系。
3.3.3 关键原则
- 统一接口:让
Composite
(容器)和Leaf
(叶子)都继承同一抽象父类,以便统一地遍历或调用。 - 递归操作:容器节点会将操作进一步委派给其子节点,直到叶子节点为止。
- 警惕循环引用:若父子互相持有
shared_ptr
,可能形成循环引用导致内存无法释放,应谨慎设计或使用weak_ptr
。
3.4 Decorator 装饰器模式
3.4.1 首选方案
- 动态地给对象添加新职责
在不改动原有类的情况下,通过“包装器(Decorator)”对象来附加额外功能。Decorator 与被装饰对象实现同一个接口,内部持有一个对原对象的引用(或指针),转发核心调用并在前后插入额外行为。 - 多层嵌套,组合功能
在 C++ 中,装饰器可以层层叠加,比如对一个流对象先包装一个“缓存装饰器”,再包装一个“压缩装饰器”,从而动态组合多种功能。
3.4.2 无奈之举
- 子类继承
若团队已习惯通过继承在子类里扩展行为,也能实现类似效果。但继承会导致类数量膨胀,而且无法在运行时灵活组合多个功能。- 过渡方式:先把某些通用功能拆到辅助类中,再在适当时机升级为装饰器结构。
3.4.3 关键原则
- 接口一致性:Decorator 与被装饰对象共享抽象接口,可互相替换或叠加。
- 前后调用:Decorator 的关键在于“调用转发”,在调用前后增加额外逻辑。
- 避免过度装饰:多层嵌套虽灵活,但也易导致调用流程复杂、调试困难。要在设计时平衡功能与可读性。
3.5 Facade 外观模式
3.5.1 首选方案
- 对复杂子系统提供简化的接口
当某个子系统内部类众多、功能繁杂,却只需对外暴露少数关键功能时,可以设计一个 Facade 类,集中与外部交互。外部系统只需调用 Facade 的方法即可,内部细节被很好地封装。 - 减少依赖
在 C++ 项目中,可把子系统的头文件集中包含在 Facade 的实现文件里,而暴露给外部的仅是 Facade 的头文件,降低编译依赖和耦合度。
3.5.2 无奈之举
- 让外部直接访问子系统
若团队默认所有功能都公开给外部调用,子系统接口会在业务代码中被频繁使用,当子系统发生改变时,所有依赖处都需修改。- 过渡方式:可以逐步将最常用或最重要的功能先封装进一个 Facade,再引导业务层使用 Facade 接口,慢慢减少对子系统直接引用。
3.5.3 关键原则
- 外观类 vs 业务逻辑:Facade 不应该承载过多业务逻辑,它只是子系统的入口。如需要大量功能组合,可考虑与其他模式结合。
- 隔离子系统变化:业务层尽量只调用 Facade;子系统内部结构调整不会影响对外接口。
- 编译优化:在 C++ 大型项目中,使用 Facade 可以减少头文件的包含量,缩短编译时间。
3.6 Flyweight 享元模式
3.6.1 首选方案
- 共享大量对象的公共部分
当系统需要创建大量细粒度对象,且这些对象间存在可共享的重复状态,可将这部分状态提取到“享元对象”中,通过缓存或工厂复用,避免重复占用内存。 - 内部状态与外部状态
在 C++ 中,一般将对象的不变(或可共享)部分放到享元对象里,而把不同个体的属性作为“外部状态”由客户端在使用时传递,减少内存消耗。
3.6.2 无奈之举
- 手动复用
如果需求不大或时间紧,可先用一个普通的 std::unordered_map 来存储已创建对象,重复使用指针。- 但这样写会出现散乱的管理逻辑,需要人工确保外部状态正确传入、避免被错误修改。
3.6.3 关键原则
- 识别可共享部分:弄清哪些数据可以在多个对象间公用,哪些必须独立。
- 缓存管理:常需要一个享元工厂负责在内存中维护共享对象的映射表。
- 适可而止:过度使用享元可能导致代码可读性变差,尤其当外部状态繁多时,很难保持逻辑清晰。
3.7 Proxy 代理模式
3.7.1 首选方案
- 在访问真实对象之前,先经过代理
代理类与真实类实现同一个接口或继承同一个基类,外部调用先进入代理,再决定是否以及如何调用真实对象。这在 C++ 中常用于远程代理(RPC 调用)、虚代理(延迟加载),或安全代理(权限校验)。 - 智能指针式代理
C++11 之后,也可以在代理模式中结合std::shared_ptr
或std::weak_ptr
来控制真实对象的创建、销毁和访问。
3.7.2 无奈之举
- 硬编码访问控制
如果只在某个函数里做了一堆“检查权限”的 if-else,然后再调用真实对象,这属于最简单的“临时代理”。- 缺点:所有地方都要重复写类似的检查逻辑,不易维护。后续如果权限规则变化,需要修改所有调用处。
3.7.3 关键原则
- 与被代理对象实现同样的接口:保证外部无需关心是代理还是真实对象。
- 延迟加载:虚代理场景中,代理可以在真正需要使用时才创建真实对象,节省性能开销。
- 完整转发:除代理所需的附加逻辑外,要保证对真实对象的调用行为保持一致,避免功能异常。
3.8 多角度对比与小结
下面这张表从“主要用途”、“实现复杂度”、“典型应用场景”和“常见风险”四个角度,对七大结构型模式做简要对比,帮助你快速回顾全貌:
模式 | 主要用途 | 实现复杂度 | 典型应用场景 | 常见风险 |
---|---|---|---|---|
Adapter | 接口转换,让原本不兼容的类能协同工作 | ★ | 新旧接口集成、第三方库适配 | 误用时可能改动过多 |
Bridge | 分离抽象与实现,让它们各自扩展 | ★★ | 多维度变化,如形状与绘制引擎 | 过度拆分导致结构复杂 |
Composite | 统一对待容器与叶子,管理树形结构 | ★★ | 图形界面、文件系统、公司组织架构 | 需谨防循环引用或生命周期问题 |
Decorator | 动态为对象增添功能,不改变原类 | ★★ | 进程中动态叠加职责,如 I/O 流过滤、UI 控件功能扩展 | 多层嵌套可能调试困难 |
Facade | 对复杂子系统提供简化接口 | ★ | 编译加速、隐藏内部实现,提供公共服务 | 界面太大可能造成新混乱 |
Flyweight | 共享对象的公共部分,减小内存开销 | ★★ | 字符渲染、大量类似对象(地图元素、棋子) | 需精细管理外部状态与缓存 |
Proxy | 在真实对象前增加一层代理控制 | ★★ | RPC 调用、权限检查、延迟加载 | 真实对象与代理逻辑不匹配风险 |
有人说,“探索结构,不过是为了理解规律,更好地驾驭变化。” 在软件工程中,结构型设计模式正是提供了这样一种可控的‘变化策略’:在宏观上看清系统依赖,微观上保证扩展与可测试性。
3.8.1 重点回顾
-
首选方案
- 适配器:隔离新旧接口,减少全局改动;
- 桥接:将抽象和实现分离,多维度扩展轻松;
- 组合:树形结构统一管理,内存与生命周期用智能指针;
- 装饰器:运行时叠加功能,更灵活而不修改原类;
- 外观:简化复杂子系统,对外只暴露核心功能;
- 享元:复用共享对象,内存占用大幅减少;
- 代理:封装访问控制,或延迟创建真实对象。
-
无奈之举
- 临时的 if-else 或继承塞在一起;
- 到处暴露底层细节、直接访问子系统;
- 在需要多层功能时依赖大量子类分支,而非动态组合;
- 或者手动管理大量共享对象,易出错。
-
关键原则
- 将易变部分封装起来:无论是接口转换、功能附加,还是层级抽象,都要封装一处,避免散落到全系统;
- 关注资源生命周期:C++ 需要考虑指针和对象引用的管理,组合、装饰、代理等模式都容易产生嵌套;
- 面向接口编程:提供统一接口层(或基类),让外部不必关心内部结构与实现细节,减少耦合。
在下一章节里,我们将继续深入探讨行为型设计模式,进一步探索在 C++ 环境下如何优化对象交互、协同工作。愿你在学习并实践结构型模式的道路上,能进一步感受到掌控系统结构所带来的从容与乐趣。
第四章: 行为型设计模式
在完成了创建型与结构型模式的探讨后,本章将聚焦行为型设计模式。这些模式主要关注对象间的职责分配与协作方式,让系统的功能更易于理解、扩展和维护。正如心理学家荣格所言:“人类的行为模式是一种深层的结构,需要借助合适的工具与方法去识别与运用。” 在软件设计里,行为型模式便是我们理解与优化“对象交互”的那把钥匙。
行为型模式常见的有以下几种:责任链(Chain of Responsibility)、命令(Command)、解释器(Interpreter)、迭代器(Iterator)、中介者(Mediator)、备忘录(Memento)、观察者(Observer)、状态(State)、策略(Strategy)、模板方法(Template Method)、访问者(Visitor)。本章依旧采用“首选方案、无奈之举、关键原则”的三段式结构进行探讨。
4.1 Chain of Responsibility(责任链)
4.1.1 首选方案
- 请求沿链条传递,链上任意节点可处理
当系统有多个处理者(Handler)可能对同一请求做出响应时,可将这些处理者串成一条链,请求从链头开始传递,直到某个节点处理完成或到达链尾。 - 节点间通过指针或智能指针连接
在 C++ 中,常在基类Handler
中保存一个std::unique_ptr<Handler>
(或原生指针)指向下一节点,利用多态来决定是否处理,以及是否将请求继续传递。
4.1.2 无奈之举
- 密集型 if-else
若在一个函数里写一连串if
来检查和处理请求,硬编码处理逻辑;后期添加或移除处理者时需改动原函数,扩展性差。- 暂时性做法:将这些
if-else
封装到一个独立的函数或类中,保证业务层不直接关心全部逻辑;后续可进一步重构为真正的责任链结构。
- 暂时性做法:将这些
4.1.3 关键原则
- 避免对具体处理者的强依赖:客户端只需把请求交给链头节点,链内部自行决定由谁来处理。
- 灵活追加或删除处理节点:可在不影响其他节点的情况下,轻松新增一个节点,满足开闭原则。
- 调试:多层节点调试较复杂,需保证请求在链中“传对地方、停对时机”。
4.2 Command(命令)
4.2.1 首选方案
- 封装请求为对象
将一个操作及其所需的参数数据,打包成“命令对象”。客户端只需调用command->execute()
,而不关心内部如何操作目标接收者。 - 撤销/重做、排队执行
在 C++ 中,命令对象可同时保存“执行逻辑”和“撤销逻辑”;如果要支持撤销,可在命令对象里实现undo()
,配合一个栈结构记录历史操作。
4.2.2 无奈之举
- 直接调用函数
如果只是简单场景,直接调用某个函数也能实现操作。但新增功能(如撤销、排队、网络发送等)会很快让调用点变得杂乱无章。- 简易过渡:至少把那些“可撤销操作”包装成一个统一的接口,后续再扩展成完整命令模式。
4.2.3 关键原则
- 行为与请求分离:命令对象将调用逻辑与调用方解耦,方便日志记录、排队、撤销等功能。
- 命令队列/历史栈:支持批量执行、延迟执行或撤销操作。
- 命令粒度:划分命令要适度,过细可能导致数目庞大,过粗又难以灵活组合。
4.3 Interpreter(解释器)
4.3.1 首选方案
- 为特定领域语言(DSL)定义语法解释
若项目中需要对某种小型语言或表达式进行解析、执行,可用解释器模式把语法规则封装为类层次,通过递归组合来解释表达式。 - 组合结构 + 递归求值
C++ 实现时,常将语法规则构造为一棵抽象语法树(AST),每个节点负责解释自己的部分,再结合子节点的结果完成整体计算。
4.3.2 无奈之举
- 写一个手工解析函数
在小型场景,可能用std::stringstream
之类手写解析逻辑,将各种分支塞进一个大函数。- 扩展困难:一旦表达式变复杂或者新增语法特性,就要反复修改该函数;可考虑渐进式抽离成“节点 + 递归解释”。
4.3.3 关键原则
- 基于类的语法表示:一个非终结符对应一个类,终结符则可作为叶子对象。
- 可读性:解释器模式在 C++ 中类数量不少,不适合过于复杂的语法;若 DSL 成熟度很高,可考虑编写正则或引入现成解析库。
- 性能考虑:解释器适用于中小规模 DSL,海量解析时可能需要更高效的编译器或虚拟机思路。
4.4 Iterator(迭代器)
4.4.1 首选方案
- 为容器提供统一遍历接口
C++ STL 已经广泛使用了迭代器概念,容器如std::vector
、std::list
都可通过迭代器统一遍历。对自定义数据结构,也可写一个迭代器类来实现operator++
、operator*
等。 - 区间语义
C++ 风格迭代器多以“首迭代器 + 尾迭代器”表示遍历区间,与std::begin()
,std::end()
搭配使用,大大简化循环逻辑。
4.4.2 无奈之举
- 在容器类里写 get(index) / size()
如果容器只提供下标访问和大小,外部需要手写 for 循环+下标,或一堆函数来访问元素。- 向迭代器过渡:先提供一个内部迭代器或自定义游标,再升级成 STL 风格迭代器,提升可读性。
4.4.3 关键原则
- 解耦遍历算法与容器本身:迭代器在容器内部可见内部结构,而客户端只拿到迭代器,不必知道细节。
- 兼容 STL:若想和标准算法配合使用,要遵守 STL 迭代器的概念(InputIterator、ForwardIterator 等)。
- 异常安全:迭代器可能因容器修改而失效,小心在循环中意外操作容器大小或结构。
4.5 Mediator(中介者)
4.5.1 首选方案
- 封装对象间复杂交互
当对象之间存在多对多的通信逻辑,且直接互相引用会造成严重耦合时,引入中介者充当“调度中心”。各对象只与中介者通信,由中介者负责协调与转发。 - 事件机制 + 中介者
在 C++ 中,中介者可以配合回调或观察者机制,统一管理对象之间的消息,让系统结构更加清晰。
4.5.2 无奈之举
- 对象相互引用、耦合蔓延
若暂未抽象出中介者,各对象可能需要彼此持有指针以调用对方。后期变动会引发“牵一发动全身”的多处改动。- 过渡做法:先把最常见的交互集中到一个“Manager”或“Coordinator”类中,后续再演进为真正的中介者模式。
4.5.3 关键原则
- 简化网状依赖:中介者模式可避免对象间直接引用,让对象能“单点依赖”中介者。
- 逻辑不宜过分集中:若中介者承担了过多业务逻辑,可能成为“上帝对象”,要警惕过度膨胀。
- 利于测试:可用 Mock 中介者来测试对象间交互,减少对整个系统环境的依赖。
4.6 Memento(备忘录)
4.6.1 首选方案
- 捕捉对象内部状态,用于恢复
当需要能“回到过去”或“撤销操作”时,可用备忘录保存对象的快照。C++ 中通常定义一个Memento
类来存储状态;对应的“Originator” 对象提供创建/恢复备忘录的函数。 - 不暴露内部实现
Memento 封装 Originator 的内部状态;可通过友元或私有接口来让 Memento 访问关键数据,而不对其他对象公开。
4.6.2 无奈之举
- 存档/恢复函数
若团队只写了一个saveToFile()
、loadFromFile()
来存储对象状态,效果类似但缺乏灵活性;也容易与外部存储耦合。- 渐进式:先将状态打包到一个“状态结构体”,然后考虑使用 Memento 思想来对接“撤销栈”或“版本管理”。
4.6.3 关键原则
- 保护访问:只有 Originator 知道如何读写 Memento,外部对象不能直接修改内部状态。
- 内存与性能:频繁创建或存储 Memento 可能耗费大量资源,需评估保存频率与差异存储机制。
- Undo/Redo 集成:Memento 常与 Command 结合实现多级撤销/重做。
4.7 Observer(观察者)
4.7.1 首选方案
- 基于回调或接口注册
当一个对象(Subject)状态变化,需要通知多个观察者(Observer)时,可将观察者注册到 Subject 中,在状态变更时遍历通知。C++ 中常用抽象接口或函数回调实现。 - 分离业务逻辑
观察者模式让 Subject 不需要关心哪些组件会对变更感兴趣,只需执行“通知”;Observer 决定如何响应。
4.7.2 无奈之举
- 轮询检查
如果没有观察者机制,有些系统只能让观察方不断轮询状态,不仅效率低,还增加耦合。- 简化方案:可先在 Subject 中加一个“事件列表”,Observer 注册回调,后续逐步抽象为完整的观察者接口。
4.7.3 关键原则
- 弱引用 vs 强引用:使用原生指针时,观察者模式易出现悬空指针或多次删除;可用
std::weak_ptr
+std::shared_ptr
方案。 - 批量通知:Subject 更新后再一次性通知所有观察者,避免在半更新状态下触发多次更新。
- 避免循环依赖:Observer 和 Subject 需清晰地管理生命周期,以防相互持有强引用。
4.8 State(状态)
4.8.1 首选方案
- 将状态封装为对象
当对象在不同状态下行为截然不同,可将每种状态独立成一个子类,使得“状态切换”只需替换当前状态对象。 - Context 持有状态指针
C++ 中可让Context
类持有一个std::unique_ptr<State>
,在运行时根据条件更换不同的具体状态子类,避免臃肿的switch
或if-else
。
4.8.2 无奈之举
- 巨大 switch-case
若单一类里根据多个状态写一堆 switch-case,修改状态代码到处都是,扩展性差且易出 bug。- 暂时性做法:先把各状态相关逻辑集中到私有函数中,再分离为若干类,逐步过渡到正宗状态模式。
4.8.3 关键原则
- 行为内聚:不同状态拥有不同行为实现,避免将所有状态逻辑混在一个大类里。
- 状态切换:可在
State
子类中决定下一状态是哪个类,或交给Context
进行管理。 - 减少耦合:Context 只知道接口类型
State
,不需要知道所有具体状态的细节。
4.9 Strategy(策略)
4.9.1 首选方案
- 抽象算法策略,运行时可替换
在 C++ 中,可把算法接口声明为抽象基类IStrategy
,具体实现写在ConcreteStrategyA/B/...
中。客户端通过setStrategy(std::unique_ptr<IStrategy>)
即可切换算法。 - 组合而非继承
策略模式通过对象组合来实现多种可替换行为,而非在一个大类中用 if-else 选择算法。
4.9.2 无奈之举
- 函数指针或模板
小规模下,直接使用函数指针或函数对象也能切换不同算法;但可读性和扩展性弱于一个清晰的策略接口。- 渐进式:可先把函数指针写在“算法配置”里,后续需求增大再替换为完整的策略类体系。
4.9.3 关键原则
- 开闭原则:想增加新算法,新增一个策略子类即可,不影响原有系统。
- 调用方只依赖抽象接口:可自由切换策略,而不修改客户端逻辑。
- 性能权衡:频繁切换策略、创建小对象时,要考虑对象分配和虚函数开销,必要时可用模板策略减少运行时开销。
4.10 Template Method(模板方法)
4.10.1 首选方案
- 在基类中定义算法骨架,延迟子类实现细节
类似于 C++ 中的抽象基类:基类提供一个final
风格的主函数templateMethod()
,其中调用若干虚函数;子类覆盖这些虚函数以实现细节,骨架逻辑保持不变。 - 代码复用
对多个相似流程的算法,避免子类重复相同流程部分,让它们只专注于特殊点的实现。
4.10.2 无奈之举
- copy-paste 多份逻辑
如果开发者在每个子类里都把类似流程的代码直接复制,时间久了维护变得非常困难,一旦流程有改动就得改一堆地方。- 短期方案:先提炼公共部分到一个基类函数,再让子类通过钩子函数插入少量差异化逻辑。
4.10.3 关键原则
- 模板方法不可被子类重写:它负责算法骨架,子类只改动钩子函数/抽象方法。
- 减少重复:公共流程固化在基类中,让子类只定制特殊步骤。
- 遵守 LSP(里氏替换原则):子类的实现不应破坏基类的既定流程,保证替换后还能正常运作。
4.11 Visitor(访问者)
4.11.1 首选方案
- 对复杂对象结构添加新操作
当系统已有复杂的类层次结构,需要频繁为这些类添加新功能时,访问者模式将“操作”从对象中分离出来,通过“双重分派”来访问具体类。 - 双分派
在 C++ 中,Visitor 模式常见实现是:对象类提供accept(Visitor&)
,在里面调用visitor.visit(*this)
;Visitor 再根据具体类型的重载处理相应逻辑。
4.11.2 无奈之举
- 在每个类里都加新方法
为实现一个新的操作,可能所有相关类都要新增相应方法,导致类定义日益膨胀。- 过渡做法:若不适合直接用访问者,可先在顶层抽象出接口
IElement
,新增某些“扩展方法”时只改IElement
,再酌情升级到访问者模式。
- 过渡做法:若不适合直接用访问者,可先在顶层抽象出接口
4.11.3 关键原则
- 数据结构相对稳定,操作变化频繁:Visitor 适合在类层次结构固定,但经常要添加新功能时。
- 双重分派:在 C++ 实现时,Visitor 必须对每个具体类提供对应的
visit()
重载,否则无法正确分派。 - 注意可扩展性:新增一个具体类,需要修改所有 Visitor;若对象类型变化也很频繁,Visitor 就显得笨重。
4.12 多角度对比与小结
下表总结了常见行为型模式的“目标场景”“复杂度”“典型应用”及“常见风险”,帮助读者在实践中快速比对和选择。
模式 | 目标场景 | 复杂度 | 典型应用场景 | 常见风险 |
---|---|---|---|---|
Chain of Responsibility | 多个处理者可处理同一请求;按顺序或条件转发 | ★★ | 事件处理管线、日志过滤器 | 调试链条流程困难 |
Command | 将操作封装成对象;支持撤销、排队、日志等 | ★★ | GUI 命令、事务系统、脚本执行 | 命令粒度设计不当导致代码膨胀 |
Interpreter | 针对小型领域语言的解析及执行 | ★★★ | 简易脚本、计算表达式、配置规则 | 类数量多,复杂 DSL 不好扩展 |
Iterator | 提供一致的方式遍历容器 | ★ | STL 容器、定制数据结构遍历 | 修改容器结构可能导致迭代器失效 |
Mediator | 简化多对象复杂交互;中介者统一协调 | ★★ | 界面控件交互、消息系统 | 中介者过度膨胀成“上帝对象” |
Memento | 备份对象内部状态,实现撤销或回溯 | ★★ | 游戏存档、编辑器 Undo/Redo | 可能消耗大量存储或内存 |
Observer | 一对多依赖,Subject 变化自动通知所有 Observer | ★★ | GUI 事件通知、数据绑定、消息推送 | 循环引用、通知风暴 |
State | 不同状态不同行为,动态切换状态对象 | ★★ | 文档编辑器状态、网络连接状态 | 仍需要清晰管理状态切换的条件 |
Strategy | 多种算法可替换,运行时可选最合适的 | ★ | 排序算法、数据压缩、路径规划 | 若算法过多,类管理复杂 |
Template Method | 在基类中定义算法骨架,子类实现可变步骤 | ★ | 各种流程控制、代码重用 | 子类改动过多时仍需修改基类 |
Visitor | 在不修改类定义的前提下,给类层次结构添加新操作 | ★★★ | 编译器语法树操作、对象序列化 | 对象结构和 Visitor 同步更新麻烦 |
“规则的存在是为了更好地打破常规。”——这句改编格言常常提醒我们,在软件设计中,模式的学习与应用是为了让开发者有更多元、更弹性的策略去应对不断变化的需求,而不是被模板或公式所束缚。
4.12.1 重点回顾
-
首选方案
- 尽量将“对象间交互”和“算法行为”封装到合适的模式结构中,避免大函数或繁琐耦合。
- C++ 中善用抽象基类 + 虚函数、智能指针、lambda 或函数对象等特性,加速实现。
-
无奈之举
- 大量 if-else 或 switch-case 处理各种行为;
- 复制黏贴多个类似流程、缺乏统一组织;
- 在对象间硬编码引用,导致耦合度过高。
-
关键原则
- 最少知识原则:让各对象只需了解其必要交互,复杂逻辑集中到特定模式(中介者、责任链等);
- 开放封闭原则:大部分行为型模式都有利于对新需求“开发”,对原有系统“封闭”,如状态、策略、访问者等;
- 合适的抽象层次:不要盲目堆叠模式,适度抽象以增强代码可维护性与可读性。
随着对行为型设计模式的探讨告一段落,本书在“创建型”、“结构型”和“行为型”三大设计模式领域的分享已基本完成。下一章(第五章)将进行综合总结,并结合一些实际项目的经验与心得,助你更好地在 C++ 开发中落地并发挥这些模式的威力。祝你在代码的世界里,在模式的指引下,挥洒更具创造性的思维!
第五章: C++ 设计模式的综合思考与实用总结
在前面几章中,我们已经依照创建型、结构型、行为型三大范畴,分别探讨了多种典型的设计模式及其在 C++ 场景下的应用方式。到了本章,我们将从更高维度回顾C++ 设计模式的共性、在实际项目中落地时应该关注的关键要点,以及一些有助于提升整体开发体验的技术实践。以下内容将以知识点与技术点为主线进行阐述。
5.1 C++ 语言特性与设计模式的紧密关联
5.1.1 RAII(Resource Acquisition Is Initialization)与智能指针
- 为什么重要:C++ 中,内存管理和资源管理是老大难问题。许多设计模式的实现都涉及对象生命周期的控制,如工厂模式返回对象指针、组合模式管理子节点等。
- 如何应用:
- 在工厂方法或抽象工厂里,优先返回
std::unique_ptr
或std::shared_ptr
,避免手动new
导致的泄漏。 - 在组合模式里,用
std::unique_ptr
储存子节点,自动负责释放,防止复杂的多处 delete。 - 在责任链或观察者等需要动态分配节点/观察者时,同样可借助智能指针减少因手动内存管理带来的复杂度。
- 在工厂方法或抽象工厂里,优先返回
5.1.2 虚函数、多态、模板
- 多态:多数设计模式(如工厂方法、策略模式、状态模式、命令模式等)都利用了虚函数与派生类的特性来实现“对接口编程”。
- 模板:
- 策略模式可通过模板参数编译期注入不同策略,避免运行时虚函数开销;这在高性能场景(如游戏引擎、图像处理)颇为常见。
- 迭代器模式在 STL 中大量依赖模板与泛型算法,使得“面向迭代器”的编程风格贯穿整个标准库。
5.1.3 继承、组合与多重继承
- 继承:许多设计模式利用单一继承结构来抽象出通用接口(抽象基类)和具体实现(派生类)。
- 组合优于继承:在 C++ 中,“对象组合”常被视为更灵活、更易维护的方式。像桥接模式、装饰器模式、代理模式都采用“指针成员”来组合某个接口对象,从而在运行时灵活替换或包装功能。
- 多重继承:C++ 支持多重继承,但要谨慎使用。对于绝大多数设计模式而言,无需多重继承即可实现。若确实需要多重继承,也最好将其中一个基类设为纯接口(仅含纯虚函数),以免出现菱形继承的复杂问题。
5.2 模式协同与演进:实际项目中的常见组合
5.2.1 “工厂 + 单例 + 抽象接口” 的综合应用
- 动机:在大型 C++ 项目中,常需要有一处“统一初始化”资源或模块的地方,同时也想兼顾可替换性与可测试性。
- 做法:
- 定义一个抽象工厂接口(提供创建某些关键对象或子系统的函数)。
- 该工厂接口在运行时,可被注入为“单例”对象,或者由一个全局函数调用该单例进行实例化。
- 测试环境可以用Mock 工厂替代真正的单例工厂,从而在单测中绕过数据库/网络等外部依赖。
5.2.2 “观察者 + 责任链 + 中介者” 在消息处理体系中的应用
- 动机:某些项目(如游戏、GUI 应用、服务器事件系统)需要对消息或事件进行分发、过滤、处理。
- 场景:
- 观察者模式可让多个模块订阅同一事件。
- 责任链模式可在“事件到达”后逐层过滤,若符合特定条件就截断处理。
- 中介者模式可将多个 UI 控件或子系统交互,集中到一个“消息中介”,从而减少对象间的耦合。
5.2.3 “状态 + 策略 + 模板方法” 帮助系统保持可扩展性
- 动机:对于流程或状态切换频繁、算法策略多变的系统,把逻辑分散到各处用 if-else 会非常难维护。
- 解决思路:
- 状态模式:将不同状态打包为不同子类,动态切换;
- 策略模式:在同一个状态下,也可能有多种算法策略可选(如难度等级、路径规划算法);
- 模板方法:把一些固定的“流程骨架”放在基类里,子类只需覆盖差异点即可。
5.3 设计模式在 C++ 项目落地时的常见挑战
5.3.1 过度设计与性能消耗
- 症状:有的团队为了“追求模式感”,将简单问题用复杂模式实现,导致类爆炸、调用层次过深,甚至引入不必要的虚函数开销、对象分配开销。
- 应对:
- 先从需求出发:评估系统扩展性、测试性、性能等要求,选用最合适的模式或简化版实现。
- profiling:在关键路径中,要衡量多态的开销与对象创建的开销,必要时可用内联函数、模板技巧、对象池等手段优化。
5.3.2 头文件依赖与编译速度
- 症状:C++ 项目常因大量头文件包含、复杂依赖而导致编译速度缓慢,尤其当设计模式中出现众多类或相互引用时,问题更为突出。
- 应对:
- 前置声明(forward declaration):在可能的情况下,避免在头文件中包含对方完整头文件,用指针或引用时只需做类的前置声明,减少不必要的编译依赖。
- PIMPL(Pointer to Implementation):对于部分模式(如 Facade、Mediator、Proxy),可以通过 PIMPL 隐藏实现细节,降低头文件耦合。
- 模块化/分层:将核心接口与实现解耦到不同的层或库文件中,借助链接阶段隔离编译影响。
5.3.3 资源与多线程同步
- 症状:现代 C++ 应用往往需要多线程,并发访问某些共享状态;传统设计模式的实现可能未考虑线程安全。
- 应对:
- 单例模式:C++11 后可以用函数内静态变量的线程安全保证,但也要考虑在多线程场景下是否会出现重入或死锁风险;
- 锁/原子操作:在责任链或观察者回调中,若多个线程可能并发通知,需要明确锁定策略;
- 无锁数据结构:对于高并发情况下“享元模式”或“缓存式工厂”等,需要更先进的无锁队列或哈希表,避免锁带来的瓶颈。
5.4 测试与持续集成中的实践
5.4.1 接口抽象与 Mock
- 动机:绝大部分设计模式都鼓励“对接口编程”,这自然而然就支持了单元测试中的Mock 对象或Stub。
- 举例:
- 在工厂方法或抽象工厂场景下,可在测试中使用 Mock 工厂来返回假的产品对象,避免对真实数据库、文件系统等依赖。
- 在观察者或中介者场景中,也能用 Mock 通信对象来测试交互逻辑。
5.4.2 断言回调与事件
- 关注点:对于命令模式、责任链、备忘录等带有较多“动态行为”的模式,测试中要考虑:
- 是否需要记录操作历史并断言正确性?
- 是否需要模拟异常或者不同行为分支,保证责任链/命令执行的完整性?
- 是否对“事件分发”频次、时机等进行检查,防止在不恰当的时间发送通知?
5.4.3 和 CI(持续集成)系统的结合
- 建议:
- 分层测试:将抽象层与实现层分离后,可以先测试抽象的接口逻辑,再测试具体实现的正确性;
- 模块化编译:设计模式在 C++ 中常有众多类与头文件,搭配 CMake 或其它构建系统时,可把这些模式实现拆分为独立静态库/动态库,降低编译耦合;
- 覆盖率与报告:可通过编译器插桩 (如
gcov
/llvm-cov
) 检查模式实现与分支覆盖率,确保各分支路径都被测试到。
5.5 学以致用:如何在项目中正确选型并落地
-
业务需求驱动
- 先对功能需求、演化需求、并发需求有清晰认识,选择或组合模式时做到“有的放矢”。
- 例如,需要对对象创建过程做大量变体或扩展时才考虑“抽象工厂/工厂方法”;需要减少类间耦合、降低通信复杂度时就考虑“中介者/观察者”等。
-
轻量化为先
- 不要追求面面俱到,避免不必要的耦合和虚函数开销;对于仅需一两个功能点的场合,可用简单封装方案替代“大而全”的模式架构。
- 如果仅是提供一次性事件通知,可能一个简单的函数指针或
std::function
回调就够了;若需求频繁演进,再升级到观察者模式。
-
不断迭代
- 设计模式在实际项目里往往经历从简单到复杂、从散乱到抽象的过程,不必一开始就大刀阔斧地套用某模式的完整形式。
- 先做好单点封装(如把一堆 if-else 集中到一个函数或类里),在条件成熟时再演进为更加通用、灵活的模式实现。
5.6 结语
C++ 设计模式是软件工程与语言特性碰撞的产物。从最基本的 RAII 到复杂的多态、模板机制,都为设计模式在 C++ 中落地提供了强有力的支撑。而各种模式之间并非割裂存在,它们时常相互协作、彼此融合,帮助开发者构建出灵活、可扩展、可维护的系统。
在实际项目中,设计模式更像是一把随身携带的工具箱。关键并不在于你是否完整遵循了某个模式的标准定义,而在于是否能将这些模式的思想融入日常开发:
- 遇到频繁变动的需求,便想起“多态 + 工厂方法/策略”的可扩展思路;
- 遇到复杂系统交互,便借助“中介者/观察者/责任链”来降低耦合;
- 遇到对象创建繁琐、资源易泄露,则借助“Builder/Singleton + RAII”来做安全管理;
- 如果不确定什么模式最合适,不妨从最小可行的封装开始,逐步演进直到满足需求。
最后,愿读者能在 C++ 语言的世界里,携手设计模式的理念,一步步走向更优雅、更高效的架构实践;也愿这些心得能在你后续的项目开发中为你带来思路上的启发和落地层面的方便。祝你在设计与代码的探索中“手中有剑,心中有光”,既能写出扎实可靠的程序,也保留对技术艺术的热爱与追求。
结语
在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。
这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。
我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。
标签:对象,接口,剖析,抽象,模式,设计模式,C++ From: https://blog.csdn.net/qq_21438461/article/details/144937715阅读我的CSDN主页,解锁更多精彩内容:泡沫的CSDN主页