023 《C++设计模式:原理、实践与现代应用》
🌟🌟🌟本文由Gemini 2.5 Flash Preview 04-17生成,用来辅助学习。🌟🌟🌟
书籍大纲
▮▮ 1. 引言:设计模式的基石
▮▮▮▮ 1.1 什么是设计模式? (What are Design Patterns?)
▮▮▮▮ 1.2 设计模式的起源与发展 (Origins and Evolution)
▮▮▮▮ 1.3 为何学习和使用设计模式? (Why Learn and Use Patterns?)
▮▮▮▮ 1.4 本书结构与阅读指南 (Book Structure and Guide)
▮▮ 2. 设计模式所需的C++基础与OOP回顾
▮▮▮▮ 2.1 类与对象 (Classes and Objects)
▮▮▮▮ 2.2 封装、继承与多态 (Encapsulation, Inheritance, and Polymorphism)
▮▮▮▮▮▮ 2.2.1 虚函数与抽象类 (Virtual Functions and Abstract Classes)
▮▮▮▮▮▮ 2.2.2 接口与实现分离 (Separation of Interface and Implementation)
▮▮▮▮ 2.3 对象组合与委托 (Object Composition and Delegation)
▮▮▮▮ 2.4 现代C++特性与设计模式 (Modern C++ Features and Patterns)
▮▮ 3. 创建型模式:对象的生成艺术
▮▮▮▮ 3.1 工厂方法模式 (Factory Method Pattern)
▮▮▮▮ 3.2 抽象工厂模式 (Abstract Factory Pattern)
▮▮▮▮ 3.3 建造者模式 (Builder Pattern)
▮▮▮▮ 3.4 原型模式 (Prototype Pattern)
▮▮▮▮ 3.5 单例模式 (Singleton Pattern)
▮▮▮▮▮▮ 3.5.1 经典的实现方法与线程安全 (Classic Implementations and Thread Safety)
▮▮▮▮▮▮ 3.5.2 现代C++中的单例实现 (Singleton Implementation in Modern C++)
▮▮ 4. 结构型模式:组织的智慧
▮▮▮▮ 4.1 适配器模式 (Adapter Pattern)
▮▮▮▮ 4.2 桥接模式 (Bridge Pattern)
▮▮▮▮ 4.3 组合模式 (Composite Pattern)
▮▮▮▮ 4.4 装饰模式 (Decorator Pattern)
▮▮▮▮ 4.5 外观模式 (Facade Pattern)
▮▮▮▮ 4.6 享元模式 (Flyweight Pattern)
▮▮▮▮ 4.7 代理模式 (Proxy Pattern)
▮▮ 5. 行为型模式:交互的智慧
▮▮▮▮ 5.1 责任链模式 (Chain of Responsibility Pattern)
▮▮▮▮ 5.2 命令模式 (Command Pattern)
▮▮▮▮ 5.3 迭代器模式 (Iterator Pattern)
▮▮▮▮▮▮ 5.3.1 C++标准库中的迭代器 (Iterators in C++ Standard Library)
▮▮▮▮ 5.4 中介者模式 (Mediator Pattern)
▮▮▮▮ 5.5 备忘录模式 (Memento Pattern)
▮▮▮▮ 5.6 观察者模式 (Observer Pattern)
▮▮▮▮▮▮ 5.6.1 基于回调函数或信号槽的实现 (Callback or Signal/Slot based Implementations)
▮▮▮▮ 5.7 状态模式 (State Pattern)
▮▮▮▮ 5.8 策略模式 (Strategy Pattern)
▮▮▮▮▮▮ 5.8.1 基于继承、函数对象和Lambda的实现 (Inheritance, Function Objects, and Lambda based Implementations)
▮▮▮▮ 5.9 模板方法模式 (Template Method Pattern)
▮▮▮▮ 5.10 访问者模式 (Visitor Pattern)
▮▮ 6. C++中的设计模式应用与实践
▮▮▮▮ 6.1 模式的组合与协同 (Combining and Collaborating Patterns)
▮▮▮▮ 6.2 设计模式与现代C++习语 (Patterns and Modern C++ Idioms)
▮▮▮▮ 6.3 性能考虑 (Performance Considerations)
▮▮▮▮ 6.4 使用设计模式进行代码重构 (Refactoring with Design Patterns)
▮▮▮▮ 6.5 C++标准库中的设计模式案例分析 (Case Studies of Patterns in C++ Standard Library)
▮▮ 7. 案例研究:构建一个实际系统
▮▮▮▮ 7.1 案例背景与需求分析 (Case Study Background and Requirements)
▮▮▮▮ 7.2 系统架构设计与模式选择 (System Architecture Design and Pattern Selection)
▮▮▮▮ 7.3 核心模块的详细设计与实现 (Detailed Design and Implementation of Core Modules)
▮▮▮▮ 7.4 测试与优化 (Testing and Optimization)
▮▮▮▮ 7.5 从案例中学习 (Lessons Learned from the Case Study)
▮▮ 8. 进阶话题与未来展望
▮▮▮▮ 8.1 反模式 (Anti-Patterns)
▮▮▮▮ 8.2 模式挖掘与领域特定模式 (Pattern Mining and Domain-Specific Patterns)
▮▮▮▮ 8.3 设计模式与软件架构模式 (Design Patterns and Software Architecture Patterns)
▮▮▮▮ 8.4 C++语言发展与设计模式 (C++ Evolution and Design Patterns)
▮▮▮▮ 8.5 总结与持续学习 (Conclusion and Continuous Learning)
▮▮ 附录A: UML基础表示法 (Basic UML Notation)
▮▮ 附录B: GoF设计模式速查表 (GoF Design Patterns Cheat Sheet)
▮▮ 附录C: 术语表 (Glossary of Terms)
▮▮ 附录D: 参考文献与推荐读物 (References and Recommended Reading)
1. 引言:设计模式的基石
欢迎来到《C++设计模式:原理、实践与现代应用》这本书。作为一名热忱的计算机科学讲师,我深信设计模式是软件开发领域中最具价值且经得起时间考验的智慧结晶之一。它们并非简单的代码技巧,而是无数优秀开发者在解决软件设计难题过程中提炼出的通用、可复用的解决方案。掌握设计模式,就像拥有了一套强大的工具箱和一套通用的语言,能够显著提升您构建复杂、健壮、灵活的C++系统的能力。
本章作为全书的开篇,旨在为您奠定坚实的基础。我们将从最根本的问题出发:什么是设计模式?它们从何而来?为何对我们如此重要?最后,我将介绍本书的结构,并为您提供一份量身定制的阅读指南,帮助您根据自身的经验水平,最大化本书的学习效果。
1.1 什么是设计模式? (What are Design Patterns?)
当我们构建软件系统时,常常会遇到一些反复出现的设计问题。例如,如何创建一个对象,使其创建过程与使用过程解耦?如何在不修改现有类的情况下扩展其功能?如何在对象之间建立一种一对多的通知机制?这些问题在不同的项目、不同的领域中一再出现,而优秀的设计师们总能找到优雅且有效的解决方案。设计模式正是这些解决方案经过抽象和整理后的成果。
简单来说,设计模式 (Design Pattern) 是在特定情境下,针对某一常见软件设计问题,提供的一套已经被验证过的、可复用的解决方案。它们描述了对象和类如何交互以及如何组成,以达到特定的设计目标。
设计模式并非可以直接拿来粘贴复制的代码库,而是一种关于如何组织代码、如何思考问题的指导思想或模板。它们比具体的算法或数据结构更抽象,因为它描述的是类与对象之间的关系和交互,关注的是系统的结构和行为。
设计模式的价值体现在以下几个方面:
⚝ 提供通用语言 (Common Vocabulary):开发者可以使用模式名称来交流复杂的思想,例如,“这里我们可以使用观察者模式 (Observer Pattern)”,这比详细描述整个机制要高效得多。
⚝ 提供经过验证的解决方案 (Proven Solutions):模式是许多有经验的开发者在实践中摸索出来的,它们往往考虑了各种潜在的问题,使用模式可以减少设计风险。
⚝ 提高软件质量 (Improve Software Quality):遵循设计模式可以使代码更易于理解、维护、重用和扩展。
⚝ 指导设计决策 (Guide Design Decisions):在面对设计难题时,设计模式可以为我们提供思考方向和备选方案。
理解设计模式的关键在于理解它们试图解决的问题 (Problem)、推荐的解决方案 (Solution) 以及应用该模式后带来的结果 (Consequences) 或权衡。
1.2 设计模式的起源与发展 (Origins and Evolution)
“设计模式”这个概念并非软件领域的原生概念。它最早来源于建筑学,由美国建筑师 Christopher Alexander 在其著作中提出,用于描述在城市和建筑设计中反复出现的、成功的解决方案。
将这一思想引入软件工程领域的是 Erich Gamma, Richard Helm, Ralph Johnson, 和 John Vlissides 这四位作者。他们通常被称为“四人帮” (Gang of Four, GoF)。1994年,他们出版了里程碑式的著作 《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)。这本书首次系统地整理和描述了23个经典的面向对象设计模式,奠定了现代软件设计模式理论的基础。
GoF的书籍对软件开发社区产生了深远的影响。它推广了设计模式的概念,提供了一套共同的语言和思考框架,极大地促进了面向对象设计思想的传播和应用。自那时起,设计模式的研究和应用持续发展,涌现出更多领域的模式,例如:
⚝ 分析模式 (Analysis Patterns):用于描述领域模型中的常见结构。
⚝ 架构模式 (Architecture Patterns):描述系统的高层组织结构,例如微服务、客户端-服务器等。
⚝ 并发模式 (Concurrency Patterns):用于解决多线程或并行编程中的问题。
⚝ 企业应用架构模式 (Patterns of Enterprise Application Architecture):如 Martin Fowler 等人总结的企业级应用设计模式。
本书主要聚焦于GoF提出的经典设计模式,并结合C++语言的特性,特别是现代C++ (Modern C++) 的发展,深入探讨如何在C++中有效地实现和应用这些模式。
1.3 为何学习和使用设计模式? (Why Learn and Use Patterns?)
或许您会问,即使不使用设计模式,我也能写出能工作的代码,那为何还要花费精力学习它们呢?答案在于:软件开发的挑战不仅仅在于“让代码工作”,更在于构建易于理解、易于修改、易于扩展、易于测试且能在团队中高效协作的代码。设计模式恰好能帮助我们应对这些挑战。
① 提升沟通效率 (Improve Communication):当团队成员都理解并使用设计模式时,他们可以使用模式名称来交流复杂的系统结构和设计意图。例如,说“我们这里可以使用工厂方法 (Factory Method) 来创建对象”,比解释整个对象创建逻辑要清晰快捷得多。这就像音乐家使用乐谱交流一样。
② 提供标准解决方案 (Provide Standard Solutions):模式是经过业界广泛实践和验证的。使用模式意味着您正在采用一种已被证明有效的方法来解决特定问题,这可以减少自己从零开始探索可能遇到的陷阱,降低项目的风险。
③ 增强软件灵活性与可维护性 (Enhance Flexibility and Maintainability):设计模式通常鼓励将系统中变化的部分与不变的部分分离 (Principle of Separate Varying Concerns),或者面向接口编程 (Program to Interfaces, not Implementations)。这使得当需求变化时,您只需修改局部代码,而不是波及整个系统,大大提高了代码的可维护性和适应性。例如,策略模式 (Strategy Pattern) 允许在运行时切换不同的算法实现,而无需修改使用算法的代码。
④ 促进代码复用 (Promote Code Reusability):模式本身是可复用的设计思想,它们指导我们构建出更模块化、更易于组合的代码组件,从而提高代码的复用率。
⑤ 帮助理解现有系统 (Aid in Understanding Existing Systems):许多大型软件系统在其设计中广泛使用了各种设计模式。理解设计模式,将使您在阅读和理解这些复杂系统时事半功倍,更快地把握其核心结构和设计意图。
⑥ 指导架构决策 (Guide Architectural Decisions):虽然GoF模式主要关注类和对象层面的设计,但其思想可以扩展到更高层的架构设计。学习模式有助于培养良好的软件设计直觉。
总之,学习设计模式是将您从“能写代码”提升到“能写出好代码”、“能进行良好设计”的关键一步。尤其是在C++这样一个强大而复杂的语言中,合理运用设计模式可以帮助您驾驭其复杂性,编写出既高效又易于管理的程序。
1.4 本书结构与阅读指南 (Book Structure and Guide)
本书旨在为您提供一个全面、系统且深入的C++设计模式学习体验。全书内容组织如下:
⚝ 第1章:引言 (您正在阅读的章节) - 介绍设计模式的基本概念、历史和重要性,以及本书的概览。
⚝ 第2章:设计模式所需的C++基础与OOP回顾 - 回顾面向对象编程 (OOP) 的核心概念 (封装、继承、多态) 以及现代C++的关键特性,这些是理解和实现设计模式的基础。
⚝ 第3章:创建型模式 (Creational Patterns) - 详细解析负责对象创建的五种模式:工厂方法、抽象工厂、建造者、原型、单例。
⚝ 第4章:结构型模式 (Structural Patterns) - 详细解析负责类和对象组合的七种模式:适配器、桥接、组合、装饰、外观、享元、代理。
⚝ 第5章:行为型模式 (Behavioral Patterns) - 详细解析负责对象间算法和交互的十一种模式:责任链、命令、迭代器、中介者、备忘录、观察者、状态、策略、模板方法、访问者。
⚝ 第6章:C++中的设计模式应用与实践 - 讨论模式的组合使用、与现代C++习语的结合、性能考量、模式在代码重构中的应用以及C++标准库中的模式实例。
⚝ 第7章:案例研究:构建一个实际系统 - 通过一个或多个综合案例,演示如何在实际项目中端到端地应用设计模式进行设计和实现。
⚝ 第8章:进阶话题与未来展望 - 探讨反模式、模式挖掘、模式与架构模式的区别与联系,以及C++语言发展对设计模式的影响。
⚝ 附录 - 提供UML基础、GoF模式速查表、术语表以及参考文献与推荐读物等实用信息。
阅读指南 🧭:
本书的设计考虑了不同经验水平的读者:
⚝ 对于初学者 (Beginners):
▮▮▮▮⚝ 确保您对C++基础知识和面向对象编程有初步了解(第2章是为您准备的)。
▮▮▮▮⚝ 重点阅读第1章,理解设计模式的核心概念和重要性。
▮▮▮▮⚝ 在学习具体模式时(第3-5章),先理解模式的意图和核心结构,不必纠结于每一个实现细节。重点关注模式解决了什么问题,以及它是如何解决的。
▮▮▮▮⚝ 案例研究(第7章)可以帮助您看到模式在实际中的应用。
▮▮▮▮⚝ 附录A和B是很好的辅助工具。
⚝ 对于中级开发者 (Intermediate):
▮▮▮▮⚝ 建议按照章节顺序阅读,本书提供了系统性的学习路径。
▮▮▮▮⚝ 在理解模式基本原理的基础上,深入研究每个模式的C++实现细节,包括不同实现方式的优缺点。
▮▮▮▮⚝ 特别关注第2章中的C++特定话题,以及第6章中模式与现代C++特性的结合、性能考量和重构技巧。
▮▮▮▮⚝ 认真研读案例研究(第7章),尝试理解设计决策背后的逻辑。
▮▮▮▮⚝ 第8章的进阶话题将拓宽您的视野。
⚝ 对于专家 (Experts):
▮▮▮▮⚝ 您可以将本书作为一本参考手册,针对性地查阅特定模式或话题。
▮▮▮▮⚝ 重点关注本书对现代C++在实现模式中的应用、模式的深层原理分析、性能考量、模式组合以及第6章和第8章的进阶内容。
▮▮▮▮⚝ 案例研究(第7章)可以作为与其他设计方案比较的参考。
▮▮▮▮⚝ 欢迎批判性地思考书中的实现方式,并结合您的经验提出更优的方案。
无论您是哪个级别的读者,我都鼓励您在阅读过程中动手实践书中的代码示例,甚至尝试将学到的模式应用到您自己的项目中。理论与实践相结合,才是掌握设计模式的王道。
准备好了吗?让我们一同踏上这段探索C++设计模式奇妙世界的旅程! ✨
2. 设计模式所需的C++基础与OOP回顾
欢迎来到本书的第二章!🎓 在深入探索奇妙的设计模式世界之前,我们需要确保你拥有坚实的C++基础,特别是对面向对象编程(Object-Oriented Programming, OOP)有深刻的理解。设计模式大多是基于面向对象的思想和特性构建的。本章将带你回顾并深入剖析那些对于理解和实现C++设计模式至关重要的概念和语言特性。无论你是初学者还是经验丰富的开发者,扎实的根基总是通往更高境界的关键。
2.1 类与对象 (Classes and Objects)
软件系统通常由相互协作的对象构成。在C++中,我们使用类(Class)作为创建对象的蓝图或模板。类定义了对象的属性(数据成员, Data Members)和行为(成员函数, Member Functions)。对象(Object)则是类的具体实例化(Instantiation)。
① 类的定义 (Class Definition)
类定义了对象的结构和接口。它封装了数据和操作这些数据的方法。
1
class Circle {
2
public:
3
// 构造函数 (Constructor)
4
Circle(double r) : radius(r) {}
5
6
// 成员函数 (Member Function) - 计算面积
7
double getArea() const {
8
return 3.14159 * radius * radius;
9
}
10
11
// 析构函数 (Destructor)
12
~Circle() {}
13
14
private:
15
// 数据成员 (Data Member)
16
double radius;
17
};
在这个例子中,Circle
是一个类。它有一个私有的数据成员 radius
和一个公有的成员函数 getArea
。构造函数 Circle(double r)
用于初始化对象,析构函数 ~Circle()
在对象生命周期结束时执行清理工作。
② 对象的创建与生命周期 (Object Creation and Lifetime)
对象的创建通常通过调用类的构造函数来完成。对象的生命周期(Lifetime)指对象从被创建到被销毁的整个过程。在C++中,对象的生命周期管理是一个核心议题,特别是在涉及资源(如内存、文件句柄、网络连接等)时。
⚝ 栈对象 (Stack Objects):在函数作用域内声明的对象通常存储在栈上。它们的生命周期由作用域决定,当作用域结束时自动销毁。
1
void func() {
2
Circle c(10.0); // 栈对象,在函数返回时自动销毁
3
// ...
4
} // c 在这里销毁
⚝ 堆对象 (Heap Objects):使用 new
关键字在堆上动态分配的对象。它们的生命周期需要程序员手动管理,通过 delete
或 delete[]
销毁。
1
Circle* p_c = new Circle(5.0); // 堆对象,需要手动销毁
2
// ...
3
delete p_c; // 手动销毁对象
4
// 忘记 delete 会导致内存泄漏 (Memory Leak) ⚠️
⚝ 全局/静态对象 (Global/Static Objects):全局对象在程序启动时创建,程序结束时销毁。静态局部对象在第一次执行到其声明处时创建,程序结束时销毁。
正确管理对象的生命周期和资源是编写健壮C++代码的基础,也是理解 RAII (Resource Acquisition Is Initialization) 等重要概念的前提,这些概念在设计模式的实现中扮演着重要角色。
2.2 封装、继承与多态 (Encapsulation, Inheritance, and Polymorphism)
面向对象编程的三大支柱——封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)——是理解和应用设计模式的核心。设计模式正是利用这些特性来解决软件设计中的共性问题。
① 封装 (Encapsulation)
封装是将数据(属性)和操作数据的方法(行为)捆绑在一起,形成一个独立的单元(对象)。同时,它通过访问修饰符(public
, protected
, private
)来控制外部对对象内部成员的访问权限,隐藏内部实现细节,只暴露必要的接口。
⚝ 优势:
▮▮▮▮⚝ 隐藏实现 (Information Hiding):使用者无需关心对象内部是如何工作的,只需了解其提供的接口。
▮▮▮▮⚝ 降低耦合 (Reduced Coupling):改变类的内部实现不会影响使用其公有接口的外部代码。
▮▮▮▮⚝ 提高可维护性 (Improved Maintainability):bug 修复或功能改进可以限定在类的内部进行。
② 继承 (Inheritance)
继承允许一个类(派生类, Derived Class 或 子类, Subclass)继承另一个类(基类, Base Class 或 父类, Superclass)的属性和行为。这促进了代码的重用(Code Reusability),并表达了“is-a”关系(例如,“狗是一种动物”)。
⚝ 继承类型:
▮▮▮▮⚝ 公有继承 (Public Inheritance):基类的公有成员在派生类中仍是公有,保护成员仍是保护。表达强烈的“is-a”关系。
▮▮▮▮⚝ 保护继承 (Protected Inheritance):基类的公有和保护成员在派生类中变为保护。
▮▮▮▮⚝ 私有继承 (Private Inheritance):基类的公有和保护成员在派生类中变为私有。更像“implemented-in-terms-of”关系。
虽然继承是OOP的重要特性,但在设计模式中,我们经常强调优先使用组合而非继承(Favor composition over inheritance),我们将在2.3节详细讨论。
③ 多态 (Polymorphism)
多态意为“多种形态”。在OOP中,多态允许使用父类指针或引用来引用子类对象,并在运行时(Runtime)调用子类中重写的(Overridden)方法。这是通过虚函数(Virtual Function)机制实现的,通常称为运行时多态(Runtime Polymorphism)或动态多态(Dynamic Polymorphism)。
⚝ 优势:
▮▮▮▮⚝ 灵活性 (Flexibility):可以处理具有共同基类的不同类型的对象,而无需知道其具体类型。
▮▮▮▮⚝ 可扩展性 (Extensibility):添加新的子类无需修改使用基类接口的现有代码。
▮▮▮▮⚝ 简化代码 (Code Simplification):通过统一的接口调用不同对象的行为。
运行时多态是许多设计模式(如策略模式、模板方法模式、观察者模式等)的基础。理解虚函数的工作原理至关重要。
2.2.1 虚函数与抽象类 (Virtual Functions and Abstract Classes)
虚函数是实现C++运行时多态的关键。
⚝ 虚函数 (Virtual Function):
在基类中使用 virtual
关键字声明的成员函数。如果派生类重写了同名、同参数列表、同返回类型的虚函数,通过基类指针或引用调用该函数时,将执行派生类中的版本。
1
class Base {
2
public:
3
virtual void display() const {
4
std::cout << "Base class display" << std::endl;
5
}
6
// ...
7
};
8
9
class Derived : public Base {
10
public:
11
void display() const override { // 使用 override 关键字表明意图,防止错误
12
std::cout << "Derived class display" << std::endl;
13
}
14
// ...
15
};
16
17
void show_display(const Base& obj) {
18
obj.display(); // 通过基类引用调用虚函数,实际执行的是对象的类型对应的版本
19
}
20
21
// Usage:
22
// Base b;
23
// Derived d;
24
// show_display(b); // Output: Base class display
25
// show_display(d); // Output: Derived class display
⚝ 纯虚函数 (Pure Virtual Function):
在虚函数声明的末尾加上 = 0
,表示这是一个纯虚函数。纯虚函数没有函数体,强制派生类必须提供该函数的实现。
1
class AbstractBase {
2
public:
3
virtual void pureVirtualFunc() = 0; // 纯虚函数
4
virtual ~AbstractBase() {} // 虚析构函数很重要!
5
};
❗ 重要提示:基类如果包含虚函数,通常应该将其析构函数声明为虚函数 (virtual ~Base() {}
),以防止在通过基类指针删除派生类对象时造成资源泄露(析构函数没有被正确调用)。
⚝ 抽象类 (Abstract Class):
包含(或继承了未实现的)至少一个纯虚函数的类被称为抽象类。抽象类不能被实例化(不能创建抽象类的对象),它主要用于定义接口(Interface)或作为其他类的基类。
2.2.2 接口与实现分离 (Separation of Interface and Implementation)
面向接口编程(Programming to an Interface)是设计模式中一个极为重要的原则。它强调依赖于抽象(接口或抽象类),而不是依赖于具体的实现细节。
⚝ 接口 (Interface):在C++中,接口通常通过只包含纯虚函数和虚析构函数的抽象类来实现。它定义了一组契约,说明一个类“能做什么”,而不关心“如何做”。
⚝ 实现 (Implementation):具体类继承接口(抽象类)并提供纯虚函数的具体实现,说明一个类“如何做”接口定义的功能。
优势:
① 解耦 (Decoupling):客户代码(Client Code)只与接口交互,不依赖于具体的实现类。这使得可以在不修改客户代码的情况下替换不同的实现。
② 灵活性和可替换性 (Flexibility and Substitutability):任何实现了相同接口的对象都可以互相替换。
③ 促进并行开发 (Facilitates Parallel Development):定义好接口后,不同团队可以并行开发依赖该接口的客户代码和实现该接口的具体类。
许多设计模式(如工厂模式、策略模式、观察者模式)都深刻体现了面向接口编程的思想。例如,工厂模式返回的是一个接口指针,客户通过接口调用方法,而不知道具体创建的是哪个实现类。
2.3 对象组合与委托 (Object Composition and Delegation)
在OOP中,除了继承,组合(Composition)是另一种构建类之间关系的方式。
⚝ 继承 (Inheritance):表示“is-a”关系。例如,Car
是 Vehicle
。一个派生类是一个基类。
⚝ 组合 (Composition):表示“has-a”关系。一个类包含另一个类的对象作为其成员。例如,Car
有一个 Engine
。
考虑以下示例:
1
// 继承示例
2
class Engine { /* ... */ };
3
class Car : public Vehicle { /* Car is a Vehicle */ /* ... */ };
4
5
// 组合示例
6
class Engine { /* ... */ };
7
class Car {
8
private:
9
Engine engine_; // Car has an Engine
10
// ...
11
public:
12
// ...
13
};
为何优先使用组合? (Favor Composition Over Inheritance)
① 降低耦合 (Reduced Coupling):组合关系中,外部类只依赖于内部成员对象的公有接口,而不是其内部实现细节。而继承则可能暴露基类的内部细节(如 protected 成员),导致更强的耦合。派生类对基类的实现细节有依赖。
② 灵活性 (Flexibility):组合关系可以在运行时动态改变被组合的对象(如果使用指针或引用),从而改变外部类的行为。继承关系在编译时就已经确定。
③ 避免多重继承的复杂性 (Avoid Multiple Inheritance Complexity):多重继承可能引入歧义和复杂性(如菱形继承问题)。组合可以更清晰地表达复杂的对象关系。
④ 更好的封装 (Better Encapsulation):组合的类通过接口使用内部成员,更好地隐藏了实现细节。
委托 (Delegation)
委托是组合的一种特殊形式,通常用于模拟继承或实现一些特定模式(如代理模式)。一个对象将它的某些职责委托给另一个对象来完成。这意味着对象 A 调用对象 B 的方法来完成自己的任务。
例如,一个 Printer
对象可能委托给一个 Formatter
对象来格式化输出文本。
1
class Formatter {
2
public:
3
virtual std::string format(const std::string& text) const = 0;
4
virtual ~Formatter() {}
5
};
6
7
class SimpleFormatter : public Formatter {
8
public:
9
std::string format(const std::string& text) const override {
10
return "[Simple] " + text;
11
}
12
};
13
14
class Printer {
15
private:
16
Formatter* formatter_; // 委托给 Formatter 对象
17
public:
18
Printer(Formatter* f) : formatter_(f) {}
19
20
void print(const std::string& text) const {
21
if (formatter_) {
22
std::cout << formatter_->format(text) << std::endl;
23
} else {
24
std::cout << text << std::endl;
25
}
26
}
27
// 注意:这里简化了生命周期管理,实际应使用智能指针
28
};
29
30
// Usage:
31
// SimpleFormatter sf;
32
// Printer printer(&sf);
33
// printer.print("Hello World"); // Output: [Simple] Hello World
委托使得对象的行为可以由其内部组合的对象决定,增强了灵活性。许多行为型设计模式(如策略模式、状态模式)的实现都使用了委托的思想。
2.4 现代C++特性与设计模式 (Modern C++ Features and Patterns)
C++11及其后续标准(如 C++14, C++17, C++20)引入了许多强大的特性,这些特性极大地改变了我们编写C++代码的方式,同时也为实现设计模式提供了新的、更安全、更高效的工具和范式。了解这些现代特性对于在C++中实践设计模式至关重要。
① 智能指针 (Smart Pointers)
手动管理内存是C++中常见的错误源。智能指针是RAII(Resource Acquisition Is Initialization)原则的应用,它们像普通指针一样工作,但在对象不再使用时自动释放资源(通常是内存)。
⚝ std::unique_ptr
:独占所有权。确保同一时间只有一个智能指针指向给定对象。当 unique_ptr
超出作用域时,它所管理的对象会被销毁。非常适合在工厂模式中返回唯一拥有权的对象。
1
#include <memory>
2
// ... 在工厂方法中 ...
3
std::unique_ptr<Product> createProduct() {
4
return std::make_unique<ConcreteProduct>(); // 返回 unique_ptr
5
}
⚝ std::shared_ptr
:共享所有权。多个 shared_ptr
可以指向同一个对象,内部使用引用计数(Reference Counting)来追踪有多少个 shared_ptr
共享同一个对象。当最后一个 shared_ptr
离开作用域时,对象被销毁。适用于多个对象需要共享访问同一个资源的情况,例如观察者模式中 Subject 持有 Observer 的 shared_ptr
。
1
#include <memory>
2
// ...
3
std::shared_ptr<Resource> res = std::make_shared<Resource>();
4
// 多个 shared_ptr 可以指向 res
5
std::shared_ptr<Resource> res2 = res; // 引用计数增加
⚝ std::weak_ptr
:弱引用。指向一个由 shared_ptr
管理的对象,但不增加引用计数。用于解决 shared_ptr
循环引用(Circular References)问题。在观察者模式中,观察者持有 Subject 的弱引用是常见做法,以避免相互持有 shared_ptr
导致内存泄漏。
使用智能指针极大地简化了许多涉及对象生命周期管理的设计模式的实现,降低了出错的可能性。
② RAII (Resource Acquisition Is Initialization)
资源获取即初始化。这是一种C++编程技术,将资源的生命周期绑定到对象的生命周期。在构造对象时获取资源,在析构对象时自动释放资源。智能指针是RAII最典型的应用之一,但RAII原则可以应用于任何需要成对操作的资源管理(如文件打开/关闭、锁获取/释放)。RAII是实现安全、健壮代码的基础,广泛应用于各种设计模式的实现细节中。
③ 移动语义 (Move Semantics)
C++11引入的移动语义(通过右值引用 &&
和移动构造函数/移动赋值运算符)允许从临时对象或将要被销毁的对象“窃取”资源,而不是进行深拷贝。这在处理大型对象或资源(如动态分配的内存)时能显著提高性能。虽然移动语义本身不是设计模式,但它影响了我们如何实现和使用某些模式,尤其是在函数返回对象或在容器中存储对象时。
④ Lambda 表达式 (Lambda Expressions)
Lambda 表达式提供了一种简洁的方式来定义匿名函数对象(Anonymous Function Objects)。它们常用于需要传递回调函数或函数对象的场景,使得行为型模式(如策略模式、命令模式、观察者模式)的实现更加灵活和简洁。
1
#include <vector>
2
#include <algorithm>
3
4
std::vector<int> nums = {1, 5, 2, 8, 3};
5
// 使用 Lambda 作为谓词 (Predicate) 对 vector 排序
6
std::sort(nums.begin(), nums.end(), [](int a, int b){
7
return a < b; // 小到大排序
8
});
在策略模式中,可以使用 Lambda 表达式直接定义算法策略,而无需创建单独的策略类。
⑤ 基于范围的 for 循环 (Range-based for Loop)
简化了对容器或序列的遍历,提高了代码的可读性。虽然不是直接与设计模式相关,但在实现模式示例或遍历模式中的对象集合时非常实用。
⑥ std::function
和 std::bind
std::function
是一个通用的函数包装器,可以存储、复制和调用任何可调用对象(函数指针、函数对象、Lambda 表达式、成员函数指针)。std::bind
用于将函数或成员函数与参数绑定。它们是实现回调机制和多态行为(不同于虚函数的多态)的有力工具,在命令模式、观察者模式等模式中非常有用。
理解并熟练运用这些现代C++特性,可以帮助你编写出更符合现代C++风格、更安全、更高效的设计模式实现。它们是连接经典设计模式理论与现代C++实践的桥梁。
本章回顾了设计模式所需的C++基础,包括OOP核心概念以及现代C++的关键特性。这些知识将作为我们深入学习各种设计模式的基石。在接下来的章节中,我们将看到这些概念是如何在具体的模式中得到应用的。
3. 创建型模式:对象的生成艺术
欢迎来到本书的第三章!👋 在前面的章节中,我们回顾了面向对象编程 (OOP) 的基础知识和C++的关键特性,这些是理解和实现设计模式的基石。从本章开始,我们将正式踏上设计模式的学习之旅。
本章,我们将聚焦于第一类重要的设计模式:创建型模式 (Creational Patterns)。顾名思义,这些模式与对象的创建过程有关。你可能会问,创建对象不就是简单地使用 new
关键字或者构造函数吗?是的,对于简单的场景来说确实如此。然而,在更复杂的软件系统中,直接实例化对象可能会引入一些问题:
⚝ 紧耦合 (Tight Coupling): 当你在客户端代码中直接使用 new ConcreteProduct()
时,你的客户端代码就与具体的 ConcreteProduct
类紧密耦合了。如果未来需要更换为另一个具体的类 (AnotherConcreteProduct
),你就必须修改客户端代码,这违反了“开闭原则” (Open/Closed Principle)——软件实体(类、模块、函数等等)应该对扩展开放,对修改封闭。
⚝ 创建逻辑复杂: 对象的创建过程可能不仅仅是一个简单的构造调用,它可能涉及多个步骤、配置选项,或者需要依赖其他对象。将这些复杂的创建逻辑直接暴露给客户端,会使得客户端代码变得臃肿且难以维护。
⚝ 灵活性不足: 有时候,你希望根据不同的条件、运行时配置,或者甚至在运行时才决定创建哪个具体类的对象。直接使用 new
关键字通常难以满足这种动态性需求。
创建型模式正是为了解决这些问题而诞生的。它们的核心思想是将对象的创建过程从使用对象 (客户端) 的代码中分离出来,从而实现创建与使用代码的解耦 (Decoupling Creation from Usage)。通过使用创建型模式,我们可以:
① 隐藏具体类名: 客户端代码只需要知道它需要一个什么“类型”或“接口”的对象,而不需要知道具体是哪个类创建了它。
② 提供创建对象的灵活方式: 可以在运行时动态地决定创建哪种对象,或者以更灵活的方式(如分步构建、通过拷贝)创建复杂对象。
③ 简化客户端代码: 客户端无需关心复杂的创建细节,只需调用一个统一的创建方法或接口。
本章将深入解析GoF (Gang of Four) 书中提到的五种创建型模式:工厂方法模式 (Factory Method Pattern)、抽象工厂模式 (Abstract Factory Pattern)、建造者模式 (Builder Pattern)、原型模式 (Prototype Pattern) 和单例模式 (Singleton Pattern)。对于每一种模式,我们都将从意图 (Intent)、动机 (Motivation)、结构 (Structure)、C++实现示例、优缺点 (Pros and Cons) 以及适用场景 (Applicability) 等多个角度进行详细探讨。
准备好了吗?让我们一起进入对象的生成艺术世界吧!🚀
3.1 工厂方法模式 (Factory Method Pattern)
3.1.1 意图 (Intent)
工厂方法模式 (Factory Method Pattern) 的核心意图是:
Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂方法模式使得一个类的实例化延迟到其子类。
简单来说,它提供了一种机制,让你在基类中定义一个创建对象的“骨架”方法(通常是虚函数),但具体的对象创建逻辑则由继承该基类的子类来实现。
3.1.2 动机 (Motivation)
设想你在开发一个游戏或一个图形编辑器,需要处理不同类型的“产品”,例如在游戏中可能有不同的“敌人” (Enemy
),在编辑器中可能有不同的“形状” (Shape
)。
最初,你可能在客户端代码中直接根据一个类型标识符来创建对象:
1
enum class ProductType { A, B, C };
2
3
// 在某个地方创建产品的代码
4
Product* createProduct(ProductType type) {
5
Product* p = nullptr;
6
switch (type) {
7
case ProductType::A:
8
p = new ConcreteProductA(); // <-- 直接依赖于 ConcreteProductA
9
break;
10
case ProductType::B:
11
p = new ConcreteProductB(); // <-- 直接依赖于 ConcreteProductB
12
break;
13
case ProductType::C:
14
p = new ConcreteProductC(); // <-- 直接依赖于 ConcreteProductC
15
break;
16
default:
17
// 处理错误
18
break;
19
}
20
return p;
21
}
这种方式的问题在于:
⚝ 如果新增一种产品类型 ProductType::D
,你不仅需要添加 ConcreteProductD
类,还需要修改 createProduct
函数,违反了开闭原则。
⚝ 客户端代码(调用 createProduct
的地方)仍然需要知道所有具体的产品类型枚举,并且依赖于这个创建函数。
工厂方法模式通过引入一个抽象的创建者 (Creator) 类和一个抽象的产品 (Product) 类来解决这个问题。抽象创建者类声明一个工厂方法,返回一个抽象产品类型的对象。具体的创建者子类覆盖 (override) 这个工厂方法,返回具体的产品子类实例。
这样,客户端代码只需要与抽象创建者和抽象产品打交道,无需知道具体的产品和具体的创建者类名。要新增产品类型,只需要新增具体产品类和对应的具体创建者类,而无需修改现有的客户端代码或抽象类。
3.1.3 结构 (Structure)
工厂方法模式的结构包含以下几个核心角色:
① Product (抽象产品):
▮▮▮▮定义工厂方法所创建的对象的接口。
② ConcreteProduct (具体产品):
▮▮▮▮实现抽象产品接口的具体类。
③ Creator (抽象创建者):
▮▮▮▮声明工厂方法,该方法返回一个 Product 类型的对象。
▮▮▮▮Creator 也可以定义一个默认的工厂方法实现,或者包含一些使用工厂方法创建产品对象的其他操作(这些操作通常是通用模板方法)。
④ ConcreteCreator (具体创建者):
▮▮▮▮覆盖 (override) Creator 中的工厂方法,返回一个 ConcreteProduct 实例。
下面是其基本结构的文本描述:
1
+--------------+ uses +---------------+
2
| <<interface>>| | <<interface>> |
3
| Product |<-------------| Creator |
4
+--------------+ +---------------+
5
| operation() | | factoryMethod(): Product|
6
+--------------+ | anOperation() |
7
^ ^
8
| implements | extends
9
+--------------+ +---------------+
10
| ConcreteProductA | | ConcreteCreatorA|
11
+--------------+ +---------------+
12
| operation() | | factoryMethod(): ConcreteProductA |
13
+--------------+ +---------------+
14
^ ^
15
| implements | extends
16
+--------------+ +---------------+
17
| ConcreteProductB | | ConcreteCreatorB|
18
+--------------+ +---------------+
19
| operation() | | factoryMethod(): ConcreteProductB |
20
+--------------+ +---------------+
3.1.4 C++ 实现示例 (C++ Implementation Example)
我们以一个简单的“日志记录器工厂”为例。我们想要一个工厂,能够创建不同类型的日志记录器,比如文件记录器 (File Logger) 和控制台记录器 (Console Logger)。
首先定义抽象产品:Logger
。
1
#include <iostream>
2
#include <string>
3
#include <memory> // For std::unique_ptr
4
5
// 抽象产品 (Abstract Product)
6
class Logger {
7
public:
8
virtual ~Logger() = default; // 虚析构函数很重要!
9
virtual void log(const std::string& message) const = 0; // 纯虚函数,定义接口
10
};
然后是具体的产品:FileLogger
和 ConsoleLogger
。
1
#include "Logger.h"
2
#include <fstream>
3
4
// 具体产品 A (Concrete Product A)
5
class FileLogger : public Logger {
6
private:
7
std::string filePath_;
8
public:
9
FileLogger(const std::string& path) : filePath_(path) {}
10
void log(const std::string& message) const override {
11
std::ofstream ofs(filePath_, std::ios::app);
12
if (ofs.is_open()) {
13
ofs << "File Log: " << message << std::endl;
14
} else {
15
std::cerr << "Error: Could not open log file " << filePath_ << std::endl;
16
}
17
}
18
};
19
20
// 具体产品 B (Concrete Product B)
21
class ConsoleLogger : public Logger {
22
public:
23
void log(const std::string& message) const override {
24
std::cout << "Console Log: " << message << std::endl;
25
}
26
};
接下来定义抽象创建者:LoggerFactory
。它声明一个虚的工厂方法 createLogger
。
1
#include "Logger.h" // Include the abstract product header
2
#include <memory>
3
4
// 抽象创建者 (Abstract Creator)
5
class LoggerFactory {
6
public:
7
virtual ~LoggerFactory() = default; // 虚析构函数
8
// 工厂方法 (Factory Method) - 返回指向抽象产品的智能指针
9
virtual std::unique_ptr<Logger> createLogger() const = 0;
10
11
// 创建者中的一些操作,使用工厂方法创建产品
12
void logMessage(const std::string& message) const {
13
// 在创建者中使用工厂方法创建产品
14
std::unique_ptr<Logger> logger = createLogger();
15
// 使用产品的功能
16
logger->log(message);
17
}
18
};
最后是具体的创建者:FileLoggerFactory
和 ConsoleLoggerFactory
。它们分别实现 createLogger
方法,返回对应的具体产品实例。
1
#include "LoggerFactory.h" // Include the abstract creator header
2
#include "FileLogger.h" // Include concrete products
3
#include "ConsoleLogger.h"
4
5
// 具体创建者 A (Concrete Creator A)
6
class FileLoggerFactory : public LoggerFactory {
7
private:
8
std::string path_;
9
public:
10
FileLoggerFactory(const std::string& path) : path_(path) {}
11
// 实现工厂方法,创建并返回 FileLogger
12
std::unique_ptr<Logger> createLogger() const override {
13
return std::make_unique<FileLogger>(path_); // 使用智能指针管理内存
14
}
15
};
16
17
// 具体创建者 B (Concrete Creator B)
18
class ConsoleLoggerFactory : public LoggerFactory {
19
public:
20
// 实现工厂方法,创建并返回 ConsoleLogger
21
std::unique_ptr<Logger> createLogger() const override {
22
return std::make_unique<ConsoleLogger>(); // 使用智能指针管理内存
23
}
24
};
客户端如何使用呢?客户端只需要知道 LoggerFactory
和 Logger
接口。它持有一个 LoggerFactory
类型的指针或引用,并通过它调用 logMessage
或直接调用 createLogger
。
1
#include "LoggerFactory.h"
2
#include "FileLoggerFactory.h"
3
#include "ConsoleLoggerFactory.h"
4
#include <vector> // For std::vector
5
6
int main() {
7
// 客户端代码与抽象工厂打交道
8
// 创建一个文件日志工厂
9
std::unique_ptr<LoggerFactory> fileFactory = std::make_unique<FileLoggerFactory>("app.log");
10
// 使用工厂创建日志器并记录消息
11
fileFactory->logMessage("This is a log message to file.");
12
13
std::cout << std::endl;
14
15
// 创建一个控制台日志工厂
16
std::unique_ptr<LoggerFactory> consoleFactory = std::make_unique<ConsoleLoggerFactory>();
17
// 使用工厂创建日志器并记录消息
18
consoleFactory->logMessage("This is a log message to console.");
19
20
// 客户端也可以直接获取产品对象
21
std::unique_ptr<Logger> directConsoleLogger = consoleFactory->createLogger();
22
directConsoleLogger->log("This message was logged via direct creation.");
23
24
// 客户端可以轻松切换工厂类型,而无需修改使用 Logger 接口的代码
25
std::vector<std::unique_ptr<LoggerFactory>> factories;
26
factories.push_back(std::make_unique<FileLoggerFactory>("another.log"));
27
factories.push_back(std::make_unique<ConsoleLoggerFactory>());
28
29
for (const auto& factory : factories) {
30
factory->logMessage("Logging from a list of factories.");
31
}
32
33
return 0;
34
}
在这个例子中,客户端代码不依赖于 FileLogger
或 ConsoleLogger
的具体类名,它只通过 Logger
接口来使用对象。要切换日志记录方式,只需要创建不同的具体工厂类实例即可,而无需修改那些调用 log
方法的代码。这就是解耦的魔力!✨
注意:在C++中,我们通常使用智能指针 (如 std::unique_ptr
或 std::shared_ptr
) 来管理由工厂方法创建的对象,以避免内存泄露问题。工厂方法应该返回智能指针,或者由调用者负责内存管理(但这不如智能指针安全和方便)。
3.1.5 优缺点 (Pros and Cons)
① 优点 (Pros):
▮▮▮▮ⓑ 解耦: 将产品的创建与使用分离开来,客户端不依赖于具体产品类。
▮▮▮▮ⓒ 开闭原则: 增加新的产品类时,只需增加对应的具体工厂类,无需修改现有的代码。
▮▮▮▮ⓓ 灵活性: 可以在子类中延迟或定制对象的创建过程。
② 缺点 (Cons):
▮▮▮▮ⓑ 复杂度增加: 每增加一个产品,就需要增加一个对应的具体工厂类,类的数量会成倍增加,使得系统更加复杂。
▮▮▮▮ⓒ 难以创建多类型产品: 一个工厂方法通常只负责创建一种类型的产品。如果要创建多种相关或相互依赖的产品,工厂方法模式就不太合适,这时可以考虑抽象工厂模式。
3.1.6 适用场景 (Applicability)
考虑使用工厂方法模式的情况:
⚝ 当一个类不知道它需要创建哪个具体类的对象时。
⚝ 当一个类希望将其创建对象的职责延迟到其子类时。
⚝ 当类库或框架需要提供一种标准方式来生成对象,并允许应用程序通过继承来自定义这些对象的类型时(框架调用工厂方法,而用户提供的代码实现具体的工厂)。
3.2 抽象工厂模式 (Abstract Factory Pattern)
3.2.1 意图 (Intent)
抽象工厂模式 (Abstract Factory Pattern) 的意图是:
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
与工厂方法模式关注创建单个产品的工厂不同,抽象工厂模式关注创建一族 (family) 产品。这些产品通常是相互关联或配合使用的。
3.2.2 动机 (Motivation)
想象你在开发一个支持多种操作系统界面库的应用程序,比如它可以运行在Windows、macOS或Linux上。每种操作系统都有自己一套独特的 UI 控件(按钮、文本框、窗口等)。这些控件在功能上是类似的(都是按钮),但在外观和行为上可能有所不同,并且它们是“一族”的,比如一个 Windows 窗口应该包含 Windows 按钮和 Windows 文本框。
如果直接在客户端代码中根据操作系统类型创建这些控件,代码会变得非常复杂和耦合:
1
enum class OSType { Windows, MacOS, Linux };
2
3
// ... 在某个地方创建 UI 控件
4
Button* createButton(OSType os) {
5
switch (os) {
6
case OSType::Windows: return new WindowsButton();
7
case OSType::MacOS: return new MacOSButton();
8
case OSType::Linux: return new LinuxButton();
9
// ...
10
}
11
return nullptr;
12
}
13
14
TextBox* createTextBox(OSType os) {
15
switch (os) {
16
case OSType::Windows: return new WindowsTextBox();
17
case OSType::MacOS: return new MacOSTextBox();
18
case OSType::Linux: return new LinuxTextBox();
19
// ...
20
}
21
return nullptr;
22
}
23
24
Window* createWindow(OSType os) {
25
switch (os) {
26
case OSType::Windows: return new WindowsWindow();
27
case OSType::MacOS: return new MacOSWindow();
28
case OSType::Linux: return new LinuxWindow();
29
// ...
30
}
31
return nullptr;
32
}
并且,你需要确保你总是创建同一“家族”的产品(例如,创建一个 Windows 窗口时,里面的按钮和文本框也必须是 Windows 风格的)。直接管理这种依赖关系很容易出错。
抽象工厂模式通过引入一个抽象的工厂 (Abstract Factory) 接口来解决这个问题,该接口声明了一系列创建不同类型产品的方法,每种方法对应族中的一个产品类型。然后,为每一族产品提供一个具体的工厂 (Concrete Factory) 类,实现抽象工厂接口,负责创建该族中的所有具体产品。
客户端代码只需要使用抽象工厂接口和抽象产品接口,无需知道具体的工厂类和具体产品类。通过切换具体的工厂对象,就可以轻松切换整个产品族。
3.2.3 结构 (Structure)
抽象工厂模式的结构包含以下核心角色:
① AbstractFactory (抽象工厂):
▮▮▮▮声明一组用于创建抽象产品对象的操作。通常每个方法对应创建族中的一个产品。
② ConcreteFactory (具体工厂):
▮▮▮▮实现 AbstractFactory 接口中声明的创建产品的方法。
▮▮▮▮每个具体工厂负责创建特定“族”的产品。
③ AbstractProduct (抽象产品):
▮▮▮▮为一类产品对象声明接口。一个产品族通常包含多种不同类型的抽象产品(如 AbstractButton
, AbstractTextBox
)。
④ ConcreteProduct (具体产品):
▮▮▮▮实现相应的 AbstractProduct 接口。
▮▮▮▮属于同一具体工厂创建的产品是同一族的。
⑤ Client (客户端):
▮▮▮▮只使用 AbstractFactory 和 AbstractProduct 接口声明的类。
下面是其基本结构的文本描述 (以UI控件为例):
1
+-------------------+ creates +-------------------+ and +-------------------+
2
| <<interface>> |---------------->| <<interface>> |------->| <<interface>> |
3
| AbstractFactory | | AbstractButton | | AbstractTextBox |
4
+-------------------+ +-------------------+ +-------------------+
5
| createButton(): AbstractButton | | paint() | | render() |
6
| createTextBox(): AbstractTextBox | +-------------------+ +-------------------+
7
+-------------------+ ^ ^
8
^ | implements | implements
9
| extends | |
10
+-------------------+ creates +-------------------+ and +-------------------+
11
| ConcreteFactoryA |---------------->| ConcreteButtonA |------->| ConcreteTextBoxA |
12
+-------------------+ +-------------------+ +-------------------+
13
| createButton(): ConcreteButtonA | | paint() | | render() |
14
| createTextBox(): ConcreteTextBoxA | +-------------------+ +-------------------+
15
+-------------------+
16
^
17
| extends
18
|
19
+-------------------+ creates +-------------------+ and +-------------------+
20
| ConcreteFactoryB |---------------->| ConcreteButtonB |------->| ConcreteTextBoxB |
21
+-------------------+ +-------------------+ +-------------------+
22
| createButton(): ConcreteButtonB | | paint() | | render() |
23
| createTextBox(): ConcreteTextBoxB | +-------------------+ +-------------------+
24
+-------------------+
25
26
+-------------------+ uses +-------------------+
27
| Client |------------->| AbstractFactory |
28
+-------------------+ +-------------------+
29
| useFactory() |
30
+-------------------+
3.2.4 C++ 实现示例 (C++ Implementation Example)
我们继续使用 UI 控件的例子。假设我们有 Windows 和 MacOS 两种风格的 UI 控件族。
首先定义抽象产品接口:Button
和 TextBox
。
1
#include <string>
2
#include <iostream>
3
#include <memory>
4
5
// 抽象产品 A (Abstract Product A)
6
class Button {
7
public:
8
virtual ~Button() = default;
9
virtual void paint() const = 0;
10
};
11
12
// 抽象产品 B (Abstract Product B)
13
class TextBox {
14
public:
15
virtual ~TextBox() = default;
16
virtual void render() const = 0;
17
virtual std::string getText() const = 0;
18
virtual void setText(const std::string& text) = 0;
19
};
然后是具体的产品:Windows 风格和 MacOS 风格的按钮和文本框。
1
#include "AbstractProducts.h"
2
3
// 具体产品 A1 (Concrete Product A1) - Windows Button
4
class WindowsButton : public Button {
5
public:
6
void paint() const override {
7
std::cout << "Rendering a Windows-style button." << std::endl;
8
}
9
};
10
11
// 具体产品 B1 (Concrete Product B1) - Windows TextBox
12
class WindowsTextBox : public TextBox {
13
private:
14
std::string text_;
15
public:
16
void render() const override {
17
std::cout << "Rendering a Windows-style textbox with text: \"" << text_ << "\"" << std::endl;
18
}
19
std::string getText() const override { return text_; }
20
void setText(const std::string& text) override { text_ = text; }
21
};
22
23
// 具体产品 A2 (Concrete Product A2) - MacOS Button
24
class MacOSButton : public Button {
25
public:
26
void paint() const override {
27
std::cout << "Rendering a MacOS-style button." << std::endl;
28
}
29
};
30
31
// 具体产品 B2 (Concrete Product B2) - MacOS TextBox
32
class MacOSTextBox : public TextBox {
33
private:
34
std::string text_;
35
public:
36
void render() const override {
37
std::cout << "Rendering a MacOS-style textbox with text: \"" << text_ << "\"" << std::endl;
38
}
39
std::string getText() const override { return text_; }
40
void setText(const std::string& text) override { text_ = text; }
41
};
接着定义抽象工厂:GUIFactory
。它声明了创建按钮和文本框的方法。
1
#include "AbstractProducts.h" // Include abstract products
2
3
// 抽象工厂 (Abstract Factory)
4
class GUIFactory {
5
public:
6
virtual ~GUIFactory() = default;
7
// 方法用于创建抽象产品 A
8
virtual std::unique_ptr<Button> createButton() const = 0;
9
// 方法用于创建抽象产品 B
10
virtual std::unique_ptr<TextBox> createTextBox() const = 0;
11
};
最后是具体的工厂:WindowsFactory
和 MacOSFactory
。它们分别实现 GUIFactory
接口,创建各自风格的具体产品。
1
#include "AbstractFactory.h" // Include abstract factory
2
#include "ConcreteProducts.h" // Include concrete products
3
4
// 具体工厂 1 (Concrete Factory 1) - Windows Factory
5
class WindowsFactory : public GUIFactory {
6
public:
7
std::unique_ptr<Button> createButton() const override {
8
return std::make_unique<WindowsButton>(); // 创建 Windows 风格按钮
9
}
10
std::unique_ptr<TextBox> createTextBox() const override {
11
return std::make_unique<WindowsTextBox>(); // 创建 Windows 风格文本框
12
}
13
};
14
15
// 具体工厂 2 (Concrete Factory 2) - MacOS Factory
16
class MacOSFactory : public GUIFactory {
17
public:
18
std::unique_ptr<Button> createButton() const override {
19
return std::make_unique<MacOSButton>(); // 创建 MacOS 风格按钮
20
}
21
std::unique_ptr<TextBox> createTextBox() const override {
22
return std::make_unique<MacOSTextBox>(); // 创建 MacOS 风格文本框
23
}
24
};
客户端代码只需要与 GUIFactory
和 Button
/TextBox
接口交互。它可以接收一个 GUIFactory
对象,然后通过这个工厂对象创建一整套产品族。
1
#include "AbstractFactory.h"
2
#include "WindowsFactory.h"
3
#include "MacOSFactory.h"
4
5
// 客户端类,使用抽象工厂
6
class Application {
7
private:
8
std::unique_ptr<GUIFactory> factory_;
9
std::unique_ptr<Button> button_;
10
std::unique_ptr<TextBox> textBox_;
11
12
public:
13
// 构造函数接收一个抽象工厂
14
Application(std::unique_ptr<GUIFactory> factory) : factory_(std::move(factory)) {
15
// 使用工厂创建产品族
16
button_ = factory_->createButton();
17
textBox_ = factory_->createTextBox();
18
}
19
20
void run() const {
21
// 使用产品族的功能
22
button_->paint();
23
textBox_->setText("Hello, Abstract Factory!");
24
textBox_->render();
25
}
26
};
27
28
int main() {
29
std::unique_ptr<GUIFactory> factory;
30
31
// 假设根据配置或运行时环境决定创建哪种工厂
32
#ifdef _WIN32
33
std::cout << "Using Windows GUI." << std::endl;
34
factory = std::make_unique<WindowsFactory>(); // 创建 Windows 产品族工厂
35
#else
36
std::cout << "Using MacOS GUI." << std::endl;
37
factory = std::make_unique<MacOSFactory>(); // 创建 MacOS 产品族工厂
38
#endif
39
40
// 客户端使用抽象工厂创建的应用
41
Application app(std::move(factory));
42
app.run(); // 运行应用,其行为取决于创建的工厂类型
43
44
return 0;
45
}
客户端 Application
类完全不依赖于 WindowsFactory
、MacOSFactory
、WindowsButton
、MacOSButton
等具体类。它只知道 GUIFactory
、Button
和 TextBox
接口。通过改变传递给 Application
构造函数的具体工厂对象,就可以在运行时切换整个 UI 风格。
注意:与工厂方法模式类似,抽象工厂的方法也应该返回智能指针以确保内存安全。
3.2.5 优缺点 (Pros and Cons)
① 优点 (Pros):
▮▮▮▮ⓑ 隔离具体类: 将客户端代码与产品对象的具体类名隔离,客户端只使用抽象接口。
▮▮▮▮ⓒ 易于切换产品族: 可以轻松地替换整个产品族(即切换具体工厂),而无需修改使用产品的客户端代码。
▮▮▮▮ⓓ 保证产品族内部的一致性: 同一个具体工厂创建的产品必然属于同一族,相互之间是协调工作的。
② 缺点 (Cons):
▮▮▮▮ⓑ 难以增加新的产品类型: 如果需要向产品族中增加一个新的产品类型(例如,新增一个 Slider
控件),你就必须修改 AbstractFactory 接口及其所有 ConcreteFactory 实现,这违反了开闭原则。
▮▮▮▮ⓒ 复杂度较高: 引入了较多的接口和类。
3.2.6 适用场景 (Applicability)
考虑使用抽象工厂模式的情况:
⚝ 当系统需要独立于其产品的创建、组合和表示时。
⚝ 当系统需要创建一系列相关或相互依赖的对象,并且希望约束这些对象属于同一族时。
⚝ 当系统提供一个产品类库,而不想暴露具体实现,只显示它们的接口时。
⚝ 当你需要根据不同的配置或环境,提供不同的产品族时。
3.3 建造者模式 (Builder Pattern)
3.3.1 意图 (Intent)
建造者模式 (Builder Pattern) 的意图是:
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
它旨在解决创建复杂对象时的混乱,特别是当对象的构造函数参数过多,或者创建步骤依赖于特定顺序时。
3.3.2 动机 (Motivation)
考虑创建一个复杂的对象,比如一个游戏角色 (GameCharacter
)。一个游戏角色有很多属性:名字、类型(战士、法师)、血量、魔法值、攻击力、防御力、拥有的装备(武器、盔甲)、技能列表等等。有些属性可能是必需的,有些是可选的,有些需要通过复杂的逻辑计算或查找来确定。
如果使用传统的构造函数方法,你可能会遇到以下问题:
⚝ “伸缩构造器” (Telescoping Constructors): 为了处理可选参数,你可能需要创建多个构造函数,每个构造函数接受不同数量的参数。例如:
GameCharacter(name, type)
GameCharacter(name, type, health)
GameCharacter(name, type, health, magic)
GameCharacter(name, type, health, magic, attack)
... 这会导致大量的构造函数,并且难以阅读和维护。
⚝ 参数过多: 如果所有属性都在一个构造函数中设置,这个构造函数将会有很多参数,容易混淆参数的顺序和含义。
⚝ 属性之间的依赖或顺序: 某些属性的设置可能依赖于其他属性,或者需要按照特定的顺序进行构建。在构造函数中管理这些复杂的依赖关系很困难。
⚝ 创建过程与对象表示耦合: 创建过程(如何一步步设置属性、添加组件)与最终的对象结构(GameCharacter
类)紧密耦合。如果创建过程有变化,可能需要修改 GameCharacter
类本身。
建造者模式通过引入一个建造者 (Builder) 对象来解决这些问题。建造者对象负责将构建复杂对象的各个步骤封装起来。它提供一系列设置对象各个部分的接口方法。一个指导者 (Director) 对象(可选)可以使用建造者接口来指导构建过程,但客户端也可以直接使用建造者。构建完成后,客户端通过建造者获取最终的产品对象。
这样,构建过程被分离到建造者类中,使得产品类本身的构造函数更简洁。同一个指导者(或者相同的构建顺序逻辑)可以使用不同的具体建造者来创建不同表示的产品。
3.3.3 结构 (Structure)
建造者模式的结构包含以下核心角色:
① Builder (抽象建造者):
▮▮▮▮为创建产品对象的各个部分声明抽象接口。
② ConcreteBuilder (具体建造者):
▮▮▮▮实现 Builder 接口,负责构建和装配产品对象的各个部分。
▮▮▮▮提供一个方法 (getResult
或 build
) 来获取最终的产品对象。
③ Director (指导者 - 可选):
▮▮▮▮构造一个使用 Builder 接口的对象。它知道使用建造者提供的哪些方法以及调用的顺序,但不知道具体建造者的实现细节。
④ Product (产品):
▮▮▮▮表示被构建的复杂对象。它包含组成对象的各个部分。ConcreteBuilder 构建的就是 Product 的实例。
下面是其基本结构的文本描述:
1
+----------------+ has a +----------------+
2
| Client |------------->| Director |
3
+----------------+ +----------------+
4
| | | construct() |
5
| | | - builder: Builder |
6
+----------------+ +----------------+
7
^ | uses
8
| |
9
+----------------+ implements +----------------+ creates +----------------+
10
| <<interface>> |<-----------------| ConcreteBuilder|---------------->| Product |
11
| Builder | +----------------+ +----------------+
12
+----------------+ | buildPartA() | | partA |
13
| buildPartA() | | buildPartB() | | partB |
14
| buildPartB() | | ... | | ... |
15
| getResult(): Product| | getResult(): Product| +----------------+
16
+----------------+ +----------------+
3.3.4 C++ 实现示例 (C++ Implementation Example)
我们来构建一个 Pizza
对象,它可能有很多配料(面团、酱料、奶酪、各种顶部配料等)。
首先定义复杂的产品:Pizza
。
1
#include <string>
2
#include <vector>
3
#include <iostream>
4
5
// 产品 (Product)
6
class Pizza {
7
private:
8
std::string dough_;
9
std::string sauce_;
10
std::string cheese_;
11
std::vector<std::string> toppings_;
12
13
public:
14
void setDough(const std::string& dough) { dough_ = dough; }
15
void setSauce(const std::string& sauce) { sauce_ = sauce; }
16
void setCheese(const std::string& cheese) { cheese_ = cheese; }
17
void addTopping(const std::string& topping) { toppings_.push_back(topping); }
18
19
void display() const {
20
std::cout << "--- Pizza ---" << std::endl;
21
std::cout << "Dough: " << dough_ << std::endl;
22
std::cout << "Sauce: " << sauce_ << std::endl;
23
std::cout << "Cheese: " << cheese_ << std::endl;
24
std::cout << "Toppings:";
25
if (toppings_.empty()) {
26
std::cout << " None";
27
} else {
28
for (const auto& topping : toppings_) {
29
std::cout << " " << topping;
30
}
31
}
32
std::cout << std::endl;
33
std::cout << "-------------" << std::endl;
34
}
35
};
接着定义抽象建造者:PizzaBuilder
。它提供构建披萨各个部分的接口。
1
#include <memory> // For std::unique_ptr
2
#include "Pizza.h" // Include the product
3
4
// 抽象建造者 (Abstract Builder)
5
class PizzaBuilder {
6
protected:
7
// 通常建造者内部会维护一个正在构建的产品对象
8
std::unique_ptr<Pizza> pizza_;
9
10
public:
11
PizzaBuilder() : pizza_(std::make_unique<Pizza>()) {}
12
virtual ~PizzaBuilder() = default;
13
14
// 抽象的构建步骤方法
15
virtual void buildDough() = 0;
16
virtual void buildSauce() = 0;
17
virtual void buildCheese() = 0;
18
// 其他可选的构建步骤,例如添加顶部配料,可能不是纯虚的,或者根据具体需求设计
19
20
// 获取最终产品的方法
21
std::unique_ptr<Pizza> getPizza() {
22
// 返回构建好的披萨,并将 pizza_ 置空,表示构建完成
23
return std::move(pizza_);
24
}
25
};
然后是具体的建造者:MargheritaPizzaBuilder
和 SpicyPizzaBuilder
,它们代表了构建不同类型披萨的具体过程。
1
#include "PizzaBuilder.h"
2
3
// 具体建造者 1 (Concrete Builder 1) - Margherita Pizza Builder
4
class MargheritaPizzaBuilder : public PizzaBuilder {
5
public:
6
void buildDough() override {
7
pizza_->setDough("Thin Crust Dough");
8
}
9
void buildSauce() override {
10
pizza_->setSauce("Tomato Sauce");
11
}
12
void buildCheese() override {
13
pizza_->setCheese("Mozzarella Cheese");
14
}
15
// 可以添加特定于此披萨类型的配料
16
void addToppings() {
17
pizza_->addTopping("Basil");
18
}
19
};
20
21
// 具体建造者 2 (Concrete Builder 2) - Spicy Pizza Builder
22
class SpicyPizzaBuilder : public PizzaBuilder {
23
public:
24
void buildDough() override {
25
pizza_->setDough("Thick Dough");
26
}
27
void buildSauce() override {
28
pizza_->setSauce("Spicy Tomato Sauce");
29
}
30
void buildCheese() override {
31
pizza_->setCheese("Pepper Jack Cheese");
32
}
33
// 可以添加特定于此披萨类型的配料
34
void addToppings() {
35
pizza_->addTopping("Pepperoni");
36
pizza_->addTopping("Jalapenos");
37
pizza_->addTopping("Onions");
38
}
39
};
可选的指导者:Waiter
(或者 Chef
),它知道如何按照标准流程制作披萨(先放面团,再放酱料,再放奶酪等),但具体的“放”什么是由建造者决定的。
1
#include "PizzaBuilder.h" // Include abstract builder
2
#include <memory>
3
4
// 指导者 (Director) - 可选
5
class Waiter {
6
private:
7
// 指导者持有抽象建造者接口
8
std::unique_ptr<PizzaBuilder> builder_;
9
10
public:
11
// 指导者通过设置具体建造者来确定要构建什么类型的披萨
12
void setBuilder(std::unique_ptr<PizzaBuilder> builder) {
13
builder_ = std::move(builder);
14
}
15
16
// 构建披萨的方法,按照固定的步骤调用建造者的方法
17
void constructPizza() {
18
if (!builder_) {
19
std::cerr << "Error: Builder not set!" << std::endl;
20
return;
21
}
22
// 构建步骤顺序固定
23
builder_->buildDough();
24
builder_->buildSauce();
25
builder_->buildCheese();
26
// 如果具体建造者有额外的特定步骤,指导者可能需要知道(这会增加耦合),
27
// 或者这些额外步骤在建造者自己的 getResult 之前完成,
28
// 或者通过基类接口调用(如果基类定义了可选步骤接口)。
29
// 在此示例中,我们将可选配料步骤放在具体建造者内部,并在 getResult 之前完成。
30
// 或者,更灵活的方式是指导者也知道如何调用可选步骤,但这打破了指导者对具体建造者无知的理想状态。
31
// 更常见的做法是让客户端/指导者知道可选步骤的存在,并选择是否调用。
32
// 为了简单起见,我们假设指导者只负责基本步骤。
33
// 如果需要更灵活的配料添加,可以在 Builder 接口中添加 addTopping 方法,并在 Director 中多次调用。
34
}
35
36
// 获取最终产品
37
std::unique_ptr<Pizza> getPizza() {
38
if (!builder_) {
39
std::cerr << "Error: Builder not set!" << std::endl;
40
return nullptr;
41
}
42
return builder_->getPizza();
43
}
44
};
客户端代码如何使用建造者模式?它可以直接使用具体建造者,也可以通过指导者来使用建造者。
1
#include "Waiter.h" // Include Director
2
#include "MargheritaPizzaBuilder.h" // Include concrete builders
3
#include "SpicyPizzaBuilder.h"
4
5
int main() {
6
// 创建指导者
7
Waiter waiter;
8
9
// 客户端方式 1: 通过指导者和具体建造者来构建
10
std::cout << "Building Margherita Pizza via Director:" << std::endl;
11
std::unique_ptr<MargheritaPizzaBuilder> margheritaBuilder = std::make_unique<MargheritaPizzaBuilder>();
12
waiter.setBuilder(std::move(margheritaBuilder)); // 设置具体建造者
13
waiter.constructPizza(); // 指导者按照固定步骤构建
14
// 注意:MargheritaPizzaBuilder::addToppings() 需要在 getPizza() 之前被调用,
15
// 可以在 constructPizza 内部调用,或者在客户端调用 getPizza 之前调用。
16
// 为了遵循 Director 知道构建过程但不知道具体实现的原则,addToppings 不应该在 Director 中调用
17
// 除非 Builder 接口中有通用的 addOptionalTopping 方法。
18
// 在这个简单的例子中,我们假设配料是在 Builder::getResult() 之前内部处理的,或者客户端直接调用。
19
// 另一种常见做法是让 Builder::getResult() 执行所有必需的构建后处理。
20
// 为了示例清晰,我们直接在 Builder 派生类中添加一个特定方法并在客户端调用(这略微增加了耦合,但更灵活)。
21
// 更好的方式是在 Builder 接口中加入更多通用的 addSomething 方法供 Director 调用。
22
// 让我们修改 ConcreteBuilders,在 getPizza 之前确保所有步骤完成,包括可选步骤。
23
// 或者,让 Builder 接口包含所有可能的构建步骤,即使某些 ConcreteBuilder 不实现某些步骤(空操作)。
24
25
// 修正 ConcreteBuilders: 在 getPizza() 之前,确保所有部分都已构建。
26
// 我们可以让 Builder::getResult() 内部执行一个 finalize 构建步骤,或者依赖 Director/客户端调用。```
27
<END_OF_CHAPTER/>
4. 结构型模式:组织的智慧
欢迎来到本书的第四章!👨🏫 在前几章,我们探讨了如何理解设计模式的基础,以及如何优雅地创建对象。本章,我们将把焦点转向软件设计的另一个重要维度:结构。
结构型模式 (Structural Patterns) 关注如何将类和对象组合起来形成更大的结构,同时保持这些结构的灵活性和效率。它们帮助我们设计具有良好组织、易于理解和维护的类与对象之间的关系。就好比建筑师设计建筑的框架和房间布局,结构型模式为我们的软件构建提供了一套经过验证的“架构蓝图”。🏛️
通过学习结构型模式,您将学会如何利用继承和组合等面向对象机制,以更智能、更灵活的方式连接不同的部分,从而构建出强大而有弹性的系统。本章将详细解析 GoF 结构型模式家族中的七个核心成员:适配器、桥接、组合、装饰、外观、享元和代理模式。我们将深入探讨它们的意图、结构、C++ 实现方式以及适用场景,并通过具体的代码示例来帮助您理解和掌握它们。
准备好了吗?让我们一起探索软件组织的智慧吧!✨
4.1 适配器模式 (Adapter Pattern)
4.1.1 什么是适配器模式?
想象一下,您有一个旧设备,它使用一种老式的接口 (interface) 连接,但您现在只有一个使用新接口的插座。怎么办?您需要一个适配器 (Adapter)!🔌 适配器模式在软件领域的作用与此类似。
意图 (Intent):将一个类的接口转换成客户希望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
简而言之,适配器模式的作用就像一个“翻译器”或“转换器”。它允许您使用一个已有的类(通常称为被适配者 (Adaptee)),即使它的接口与您期望的(即目标接口 (Target Interface))不兼容。适配器会包装被适配者,并以目标接口的形式暴露其功能。客户代码 (Client) 只需要知道目标接口即可与被适配者交互,而无需关心底层实现的细节。
4.1.2 适配器模式的结构与参与者
适配器模式通常有两种实现形式:类适配器 (Class Adapter) 和对象适配器 (Object Adapter)。
类适配器 (Class Adapter):
⚝ 使用多重继承实现。适配器类同时继承目标接口和被适配者类。
⚝ 直接继承被适配者的行为,并在适配器中实现目标接口的方法,这些方法内部调用被适配者的相应功能。
⚝ 要求被适配者必须是一个具体的类,而不是接口(在 C++ 中通常是具体的类或实现了某个接口的类),并且不能是 final
的。
⚝ 在 C++ 中,由于私有继承 (private inheritance) 表示“基于...实现”,而公有继承 (public inheritance) 表示“是...的一种类型”,类适配器通常通过公有继承目标接口和私有继承被适配者来实现。
结构图 (UML 描述):
1
classDiagram
2
direction LR
3
class Target {
4
<<interface>>
5
+Request()
6
}
7
class Adaptee {
8
+SpecificRequest()
9
}
10
class ClassAdapter {
11
+Request()
12
}
13
14
Target <|-- ClassAdapter
15
Adaptee <|-- ClassAdapter
16
17
ClassAdapter ..> Adaptee : calls
参与者 (Participants):
⚝ 目标 (Target): 客户所期望的接口。客户通过这个接口与适配器交互。在 C++ 中,通常是一个抽象基类 (abstract base class) 或纯虚类。
⚝ 被适配者 (Adaptee): 已有的、需要被适配的类。其接口与目标接口不兼容。
⚝ 类适配器 (ClassAdapter): 实现了目标接口,并继承了被适配者。它将调用目标接口方法的请求转发给被适配者的相应方法。
对象适配器 (Object Adapter):
⚝ 使用对象组合实现。适配器类实现目标接口,并持有一个被适配者对象的引用或指针。
⚝ 通过委托 (delegation) 的方式,将目标接口方法的调用转发给其内部持有的被适配者对象。
⚝ 更灵活,可以适配被适配者的子类,也可以在运行时切换被适配者实例。这是更常用的实现方式。
结构图 (UML 描述):
1
classDiagram
2
direction LR
3
class Target {
4
<<interface>>
5
+Request()
6
}
7
class Adaptee {
8
+SpecificRequest()
9
}
10
class ObjectAdapter {
11
+Request()
12
}
13
14
Target <|-- ObjectAdapter
15
ObjectAdapter "1" *--> "1" Adaptee : aggregates
16
17
ObjectAdapter ..> Adaptee : calls
参与者 (Participants):
⚝ 目标 (Target): 同类适配器。
⚝ 被适配者 (Adaptee): 同类适配器。
⚝ 对象适配器 (ObjectAdapter): 实现了目标接口,并包含一个被适配者对象的实例。它将调用目标接口方法的请求委托给其内部持有的被适配者对象的相应方法。
4.1.3 C++ 实现示例
我们将展示对象适配器的 C++ 实现,因为它更常见且更灵活。
假设我们有一个遗留系统中的 LegacyRectangle
类,它使用 draw(x1, y1, x2, y2)
方法绘制矩形,坐标表示左上角和右下角。
1
// 被适配者 (Adaptee)
2
class LegacyRectangle {
3
public:
4
void draw(int x1, int y1, int x2, int y2) {
5
std::cout << "LegacyRectangle::draw(" << x1 << ", " << y1 << ", " << x2 << ", " << y2 << ") called." << std::endl;
6
// ... actual drawing logic
7
}
8
};
而我们现在希望使用一个统一的 Shape
接口,它使用 draw(x, y, width, height)
方法绘制图形,坐标表示左上角、宽度和高度。
1
// 目标接口 (Target)
2
class Shape {
3
public:
4
virtual ~Shape() = default;
5
virtual void draw(int x, int y, int width, int height) = 0;
6
};
我们需要一个适配器来让 LegacyRectangle
工作在 Shape
接口下。
1
#include <iostream>
2
#include <memory> // For std::unique_ptr or std::shared_ptr
3
4
// 被适配者 (Adaptee)
5
class LegacyRectangle {
6
public:
7
void draw(int x1, int y1, int x2, int y2) {
8
std::cout << "LegacyRectangle::draw(" << x1 << ", " << y1 << ", " << x2 << ", " << y2 << ") called." << std::endl;
9
// ... actual drawing logic
10
}
11
};
12
13
// 目标接口 (Target)
14
class Shape {
15
public:
16
virtual ~Shape() = default;
17
virtual void draw(int x, int y, int width, int height) = 0;
18
};
19
20
// 对象适配器 (Object Adapter)
21
class RectangleAdapter : public Shape {
22
private:
23
// 适配器持有被适配者对象
24
std::unique_ptr<LegacyRectangle> legacyRect_;
25
26
public:
27
// 构造函数,接收被适配者对象或创建被适配者对象
28
RectangleAdapter() : legacyRect_(std::make_unique<LegacyRectangle>()) {}
29
30
// 实现目标接口的 draw 方法
31
void draw(int x, int y, int width, int height) override {
32
std::cout << "RectangleAdapter::draw(" << x << ", " << y << ", " << width << ", " << height << ") called." << std::endl;
33
// 将新接口的坐标转换为旧接口的坐标,并调用被适配者的方法
34
int x1 = x;
35
int y1 = y;
36
int x2 = x + width;
37
int y2 = y + height;
38
legacyRect_->draw(x1, y1, x2, y2);
39
}
40
};
41
42
// 客户代码 (Client)
43
void drawShape(Shape& shape) {
44
shape.draw(10, 20, 50, 100);
45
}
46
47
int main() {
48
RectangleAdapter adapter;
49
drawShape(adapter);
50
51
return 0;
52
}
运行结果:
1
RectangleAdapter::draw(10, 20, 50, 100) called.
2
LegacyRectangle::draw(10, 20, 60, 120) called.
在这个示例中,RectangleAdapter
作为适配器,实现了 Shape
接口,并在其 draw
方法中创建并使用了 LegacyRectangle
对象。它负责将新的坐标表示 (x, y, width, height) 转换成旧的坐标表示 (x1, y1, x2, y2),然后调用 LegacyRectangle
的 draw
方法。客户代码 drawShape
不知道也不关心它实际操作的是一个 LegacyRectangle
,它只需要通过统一的 Shape
接口进行调用。
4.1.4 优缺点与适用场景
优点 (Pros):
⚝ 提高类的复用性 (Reusability): 可以将已有的类用于新的环境,而无需修改其源代码。
⚝ 提高系统的灵活性和扩展性 (Flexibility and Extensibility): 客户代码与被适配者解耦,新增被适配者或适配器不会影响客户代码。
⚝ 隐藏实现细节 (Hides Implementation Details): 客户只看到目标接口,不知道被适配者的具体实现。
缺点 (Cons):
⚝ 增加了代码复杂度 (Increased Complexity): 引入了新的类(适配器),可能会增加系统的复杂性。
⚝ 类适配器的限制 (Class Adapter Limitations): 类适配器依赖于多重继承,可能会引入一些继承带来的问题(如菱形继承问题),并且只能适配具体的类。
⚝ 性能开销 (Performance Overhead): 每次调用都需要经过适配器转发,可能会带来微小的性能开销(通常可以忽略不计)。
适用场景 (Applicability):
⚝ 您想使用一个已有的类,但其接口与您的系统不兼容时。
⚝ 您想创建一个可复用的类,该类可以与一些不相关的或不可预见的类(这些类接口可能不兼容)协同工作时。
⚝ (仅限类适配器)您想创建一个适配器,但又不想额外引入一个对象来持有被适配者,并且被适配者是具体的类。
4.1.5 相关模式
⚝ 桥接模式 (Bridge Pattern): 桥接模式关注的是将抽象与其实现解耦,使得它们可以独立变化。适配器模式关注的是使两个已有、不兼容的接口能够协同工作。适配器解决的是“事后”的接口不匹配问题,而桥接模式解决的是“事前”的抽象与实现的分离设计问题。
⚝ 外观模式 (Facade Pattern): 外观模式为子系统提供一个简化的统一接口,隐藏子系统的复杂性。适配器模式关注的是转换一个接口以匹配另一个接口,通常只涉及两个类之间的关系,而外观模式涉及一个接口与一个子系统之间的关系。
⚝ 代理模式 (Proxy Pattern): 代理模式为另一个对象提供一个替身,并控制对这个对象的访问。虽然适配器和代理都包装了另一个对象,但它们的目的是不同的:适配器改变接口,代理控制访问或延迟对象的创建。
4.2 桥接模式 (Bridge Pattern)
4.2.1 什么是桥接模式?
桥接模式的核心思想是“将抽象与实现分离,使它们都可以独立地变化”。想象一下,您正在设计一套图形绘制系统。您可能有不同的图形形状 (Shape),比如圆形 (Circle)、方形 (Square);您也可能有不同的绘图设备 (Drawing Device),比如屏幕 (Screen)、打印机 (Printer)。如果直接将形状与设备耦合,您可能需要为每种形状在每种设备上都实现一个绘制方法,导致类的数量爆炸:ScreenCircleDrawer
, PrinterCircleDrawer
, ScreenSquareDrawer
, PrinterSquareDrawer
... 当新增一种形状或一种设备时,工作量会非常大。
桥接模式就像在形状的抽象概念和绘图设备的实现之间架起一座“桥梁”。形状不再直接绘制自己,而是通过一个指向绘图设备实现的引用来完成绘制。这样,您可以独立地增加新的形状或新的绘图设备,而无需修改现有的代码。
意图 (Intent):将抽象部分与它的实现部分分离,使它们都可以独立地变化。
桥接模式将一个大的类或相互关联的类层次结构分解为两个独立的层次结构:抽象 (Abstraction) 和 实现 (Implementation)。抽象层定义了客户端使用的接口(即“是什么”),而实现层包含了不同平台或方式的具体实现(即“怎么做”)。抽象层持有实现层的对象的引用,所有客户端的请求都委托给实现层。
4.2.2 桥接模式的结构与参与者
桥接模式的结构包括四个主要角色:
结构图 (UML 描述):
1
classDiagram
2
direction LR
3
class Abstraction {
4
+Operation()
5
#implementor : Implementor
6
}
7
class RefinedAbstraction {
8
+Operation()
9
}
10
class Implementor {
11
<<interface>>
12
+OperationImpl()
13
}
14
class ConcreteImplementorA {
15
+OperationImpl()
16
}
17
class ConcreteImplementorB {
18
+OperationImpl()
19
}
20
21
Abstraction o-- Implementor : aggregates
22
Abstraction <|-- RefinedAbstraction
23
Implementor <|-- ConcreteImplementorA
24
Implementor <|-- ConcreteImplementorB
25
26
Abstraction ..> Implementor : calls
参与者 (Participants):
⚝ 抽象 (Abstraction): 定义了抽象的接口。它维护一个指向 Implementor
类型对象的引用。客户端通过这个接口与系统交互。在 C++ 中通常是一个抽象基类。
⚝ 细化抽象 (RefinedAbstraction): 扩展或实现了 Abstraction
接口。它提供了一种抽象的具体实现。可以有多个细化抽象。
⚝ 实现者 (Implementor): 定义了实现类的接口。这个接口不必与 Abstraction
的接口完全对应。事实上,这两个接口通常是不同的,Implementor
接口只提供基本操作,而 Abstraction
接口提供基于这些基本操作的更高级操作。在 C++ 中通常是一个抽象基类。
⚝ 具体实现者 (ConcreteImplementorA, ConcreteImplementorB, ...): 实现 Implementor
接口的具体类。可以有多个具体实现者。
桥接模式的关键在于 Abstraction
维护一个 Implementor
的引用,并且 Abstraction
的操作方法会调用其内部持有的 Implementor
对象的对应方法。这样,Abstraction
的变化(增加 RefinedAbstraction
)和 Implementor
的变化(增加 ConcreteImplementor
)可以独立进行。
4.2.3 C++ 实现示例
我们沿用上面的图形绘制系统的例子。
Implementor
接口:绘图设备。
1
// 实现者接口 (Implementor)
2
class DrawingAPI {
3
public:
4
virtual ~DrawingAPI() = default;
5
virtual void drawCircle(double x, double y, double radius) = 0;
6
virtual void drawRectangle(double x, double y, double width, double height) = 0;
7
// ... 可以添加其他绘制基本图形的方法
8
};
ConcreteImplementor
:具体的绘图设备实现。
1
// 具体实现者 A (ConcreteImplementor A)
2
class ScreenDrawingAPI : public DrawingAPI {
3
public:
4
void drawCircle(double x, double y, double radius) override {
5
std::cout << "Drawing Circle on Screen: (" << x << ", " << y << ") with radius " << radius << std::endl;
6
// ... screen drawing logic
7
}
8
void drawRectangle(double x, double y, double width, double height) override {
9
std::cout << "Drawing Rectangle on Screen: (" << x << ", " << y << ") with dimensions " << width << "x" << height << std::endl;
10
// ... screen drawing logic
11
}
12
};
13
14
// 具体实现者 B (ConcreteImplementor B)
15
class PrinterDrawingAPI : public DrawingAPI {
16
public:
17
void drawCircle(double x, double y, double radius) override {
18
std::cout << "Drawing Circle on Printer: (" << x << ", " << y << ") with radius " << radius << std::endl;
19
// ... printer drawing logic
20
}
21
void drawRectangle(double x, double y, double width, double height) override {
22
std::cout << "Drawing Rectangle on Printer: (" << x << ", " << y << ") with dimensions " << width << "x" << height << std::endl;
23
// ... printer drawing logic
24
}
25
};
Abstraction
接口:图形形状。它持有 DrawingAPI
的引用。
1
// 抽象 (Abstraction)
2
class Shape {
3
protected:
4
// Shape 持有 DrawingAPI 的引用
5
std::unique_ptr<DrawingAPI> drawingAPI_;
6
7
public:
8
// 构造函数接收一个 DrawingAPI 实现
9
Shape(std::unique_ptr<DrawingAPI> api) : drawingAPI_(std::move(api)) {}
10
virtual ~Shape() = default;
11
12
// 抽象的绘制方法,由子类实现,但内部会调用 drawingAPI_
13
virtual void draw() = 0;
14
};
RefinedAbstraction
:具体的图形形状实现。
1
// 细化抽象 A (RefinedAbstraction A)
2
class Circle : public Shape {
3
private:
4
double x_, y_, radius_;
5
6
public:
7
Circle(double x, double y, double radius, std::unique_ptr<DrawingAPI> api)
8
: Shape(std::move(api)), x_(x), y_(y), radius_(radius) {}
9
10
// 实现 Shape 的 draw 方法,委托给 drawingAPI_
11
void draw() override {
12
drawingAPI_->drawCircle(x_, y_, radius_);
13
}
14
};
15
16
// 细化抽象 B (RefinedAbstraction B)
17
class Rectangle : public Shape {
18
private:
19
double x_, y_, width_, height_;
20
21
public:
22
Rectangle(double x, double y, double width, double height, std::unique_ptr<DrawingAPI> api)
23
: Shape(std::move(api)), x_(x), y_(y), width_(width), height_(height) {}
24
25
// 实现 Shape 的 draw 方法,委托给 drawingAPI_
26
void draw() override {
27
drawingAPI_->drawRectangle(x_, y_, width_, height_);
28
}
29
};
客户代码 (Client) 可以创建不同的形状,并为它们指定不同的绘图设备。
1
int main() {
2
// 创建一个在屏幕上绘制的圆形
3
std::unique_ptr<Shape> screenCircle = std::make_unique<Circle>(1.0, 2.0, 5.0, std::make_unique<ScreenDrawingAPI>());
4
screenCircle->draw();
5
6
// 创建一个在打印机上绘制的方形
7
std::unique_ptr<Shape> printerRectangle = std::make_unique<Rectangle>(3.0, 4.0, 6.0, 8.0, std::make_unique<PrinterDrawingAPI>());
8
printerRectangle->draw();
9
10
// 创建一个在屏幕上绘制的方形
11
std::unique_ptr<Shape> screenRectangle = std::make_unique<Rectangle>(5.0, 6.0, 7.0, 9.0, std::make_unique<ScreenDrawingAPI>());
12
screenRectangle->draw();
13
14
return 0;
15
}
运行结果:
1
Drawing Circle on Screen: (1, 2) with radius 5
2
Drawing Rectangle on Printer: (3, 4) with dimensions 6x8
3
Drawing Rectangle on Screen: (5, 6) with dimensions 7x9
通过桥接模式,我们成功地将图形形状的抽象与绘图设备的实现分离。增加新的形状(如 Triangle)只需要创建新的 Shape
子类,而无需关心绘图设备的细节;增加新的绘图设备(如 VectorDrawingAPI)只需要创建新的 DrawingAPI
子类,而无需修改现有的形状类。这极大地提高了系统的灵活性和可维护性。
在 C++ 中,桥接模式常与 Pimpl (Pointer to Implementation) 习语结合使用,用于隐藏类的私有实现细节,减少头文件依赖,加快编译速度。这也体现了抽象与实现的分离。
4.2.4 优缺点与适用场景
优点 (Pros):
⚝ 分离抽象与实现 (Separates Abstraction and Implementation): 使它们可以独立演化,避免了类的层次结构的无限膨胀。
⚝ 提高了系统的可扩展性 (Improved Extensibility): 可以独立增加新的抽象或新的实现。
⚝ 减少了实现类的数量 (Reduces Number of Implementation Classes): 相较于直接耦合,可以显著减少类的数量。
缺点 (Cons):
⚝ 增加了系统的复杂性 (Increased Complexity): 引入了两个层次结构,理解和设计起来比直接实现要复杂一些。
⚝ 可能增加通信开销 (Potential Communication Overhead): 抽象层通过委托调用实现层的方法,可能带来一些间接性开销(通常可以忽略)。
适用场景 (Applicability):
⚝ 您不希望在抽象和实现之间使用永久的绑定关系。抽象和实现都应该可以独立地变化。
⚝ 类的抽象和它的实现都应该通过继承机制加以扩展。如果直接耦合,会导致继承的类爆炸式增长。
⚝ 需要在运行时切换一个对象的实现。
⚝ 避免在抽象层中暴露实现细节。
4.2.5 相关模式
⚝ 抽象工厂模式 (Abstract Factory Pattern): 抽象工厂模式可以用来创建桥接模式中的具体实现者对象。
⚝ 适配器模式 (Adapter Pattern): 桥接模式是先于代码编写进行的设计决策,用于分离抽象和实现;适配器模式是事后应用的,用于解决接口不兼容问题。有时候,桥接模式可以使用适配器来实现其 Implementor
接口,以便利用已有的类作为实现。
⚝ 策略模式 (Strategy Pattern): 策略模式也使用对象组合来委托行为,但它主要用于定义一系列可互相替换的算法,而桥接模式主要用于分离抽象和实现,以实现它们各自的独立变化。
4.3 组合模式 (Composite Pattern)
4.3.1 什么是组合模式?
设想一下,您正在构建一个文件系统的数据结构。文件系统既包含单个文件,也包含文件夹,而文件夹里面又可以包含其他文件或文件夹。您希望对文件和文件夹进行统一的操作(比如显示名称、计算大小等)。如果您区分对待文件和文件夹,那么遍历整个文件系统结构会非常复杂。
组合模式 (Composite Pattern) 就像构建一个树形结构,其中包含两种类型的对象:叶子 (Leaf) 节点(代表单个对象,如文件)和组合 (Composite) 节点(代表组合对象,如文件夹)。组合节点内部包含其他叶子节点或组合节点。最重要的是,叶子节点和组合节点都实现了相同的接口,这样客户端可以对两者进行统一的操作,无需区分它们是单个对象还是组合对象。
意图 (Intent):将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
这个模式的关键在于定义一个抽象的组件 (Component) 接口,它声明了叶子节点和组合节点共同的操作。叶子节点实现了这个接口,而组合节点也实现了这个接口,并且在其实现中,通常会遍历其内部的孩子节点 (Children),并调用孩子节点的相同方法(递归)。
4.3.2 组合模式的结构与参与者
组合模式的结构包括三个主要角色:
结构图 (UML 描述):
1
classDiagram
2
direction LR
3
class Component {
4
+Operation()
5
+Add(Component)
6
+Remove(Component)
7
+GetChild(int)
8
}
9
class Leaf {
10
+Operation()
11
}
12
class Composite {
13
+Operation()
14
+Add(Component)
15
+Remove(Component)
16
+GetChild(int)
17
}
18
19
Component <|-- Leaf
20
Component <|-- Composite
21
Composite "1" *--> "*" Component : contains
参与者 (Participants):
⚝ 组件 (Component): 为组合中的对象定义统一的接口,包括所有叶子和组合都应实现的方法。它可能也包含一些管理孩子组件的方法(如 Add()
, Remove()
, GetChild()
)。在 C++ 中通常是一个抽象基类。
⚝ 叶子 (Leaf): 表示组合中的叶节点对象,没有子节点。它们实现了 Component
接口中的基本操作。
⚝ 组合 (Composite): 表示组合中的分支节点对象,可以包含子节点(即其他 Component
对象)。它实现了 Component
接口,并维护一个子节点的集合。其操作通常会递归地调用其子节点的相应操作。
注意: Component
接口中是否包含管理子节点的方法 (Add
, Remove
, GetChild
) 是一个设计上的权衡。
① 如果包含,客户端可以使用统一的方式操作叶子和组合,但叶子类需要提供这些方法的空实现或抛出异常(因为叶子不能有子节点),这违反了接口隔离原则 (Interface Segregation Principle)。
② 如果不包含,客户端需要区分叶子和组合来调用管理子节点的方法,这牺牲了一致性,但更符合接口隔离原则。
实际应用中,通常会选择第一种方式,因为它使得客户端代码更简单。本示例也将采用这种方式。
4.3.3 C++ 实现示例
我们来实现文件系统的例子。
Component
接口:FileSystemComponent
,表示文件或文件夹的抽象。
1
#include <iostream>
2
#include <vector>
3
#include <string>
4
#include <memory> // For std::unique_ptr
5
#include <stdexcept> // For std::runtime_error
6
7
// 组件 (Component)
8
class FileSystemComponent {
9
protected:
10
std::string name_;
11
12
public:
13
FileSystemComponent(const std::string& name) : name_(name) {}
14
virtual ~FileSystemComponent() = default;
15
16
const std::string& getName() const {
17
return name_;
18
}
19
20
// 所有组件都共有的操作
21
virtual void display(int indent = 0) const = 0;
22
23
// 管理孩子节点的方法(在 Component 中定义,叶子节点可以抛异常或空实现)
24
virtual void add(std::unique_ptr<FileSystemComponent> component) {
25
throw std::runtime_error("Cannot add component to a non-composite.");
26
}
27
virtual void remove(const std::string& name) {
28
throw std::runtime_error("Cannot remove component from a non-composite.");
29
}
30
virtual FileSystemComponent* getChild(const std::string& name) const {
31
return nullptr;
32
}
33
};
Leaf
类:File
,表示文件。
1
// 叶子 (Leaf)
2
class File : public FileSystemComponent {
3
private:
4
int size_;
5
6
public:
7
File(const std::string& name, int size) : FileSystemComponent(name), size_(size) {}
8
9
// 实现 display 方法
10
void display(int indent = 0) const override {
11
for (int i = 0; i < indent; ++i) std::cout << " ";
12
std::cout << "📄 File: " << name_ << " (" << size_ << " KB)" << std::endl;
13
}
14
15
// 叶子节点不能有子节点,这里可以不重写 add/remove/getChild
16
// 或者重写并抛出异常,基类已经提供了默认的抛异常实现
17
};
Composite
类:Folder
,表示文件夹。
1
// 组合 (Composite)
2
class Folder : public FileSystemComponent {
3
private:
4
std::vector<std::unique_ptr<FileSystemComponent>> children_;
5
6
public:
7
Folder(const std::string& name) : FileSystemComponent(name) {}
8
9
// 实现 display 方法,递归调用子节点的 display
10
void display(int indent = 0) const override {
11
for (int i = 0; i < indent; ++i) std::cout << " ";
12
std::cout << "📁 Folder: " << name_ << std::endl;
13
14
for (const auto& child : children_) {
15
child->display(indent + 1); // 递归调用
16
}
17
}
18
19
// 重写管理孩子节点的方法
20
void add(std::unique_ptr<FileSystemComponent> component) override {
21
children_.push_back(std::move(component));
22
}
23
24
void remove(const std::string& name) override {
25
// 查找并移除子节点
26
for (auto it = children_.begin(); it != children_.end(); ++it) {
27
if ((*it)->getName() == name) {
28
children_.erase(it);
29
std::cout << "Removed component: " << name << std::endl;
30
return;
31
}
32
}
33
std::cout << "Component not found: " << name << std::endl;
34
}
35
36
FileSystemComponent* getChild(const std::string& name) const override {
37
for (const auto& child : children_) {
38
if (child->getName() == name) {
39
return child.get();
40
}
41
// 如果子节点也是文件夹,则递归查找
42
if (Folder* subFolder = dynamic_cast<Folder*>(child.get())) {
43
if (FileSystemComponent* found = subFolder->getChild(name)) {
44
return found;
45
}
46
}
47
}
48
return nullptr;
49
}
50
};
客户端代码 (Client) 使用统一的 FileSystemComponent
指针来操作文件和文件夹。
1
int main() {
2
// 构建文件系统结构
3
std::unique_ptr<FileSystemComponent> root = std::make_unique<Folder>("Root");
4
5
std::unique_ptr<FileSystemComponent> documents = std::make_unique<Folder>("Documents");
6
documents->add(std::make_unique<File>("Report.docx", 500));
7
documents->add(std::make_unique<File>("MeetingNotes.txt", 150));
8
9
std::unique_ptr<FileSystemComponent> images = std::make_unique<Folder>("Images");
10
images->add(std::make_unique<File>("Photo1.jpg", 2000));
11
images->add(std::make_unique<File>("Photo2.png", 1200));
12
13
std::unique_ptr<FileSystemComponent> readme = std::make_unique<File>("README.md", 300);
14
15
root->add(std::move(documents));
16
root->add(std::move(images));
17
root->add(std::move(readme));
18
19
// 显示整个文件系统结构 (统一操作)
20
std::cout << "Displaying file system structure:" << std::endl;
21
root->display();
22
23
std::cout << "\n--- Removing Images folder ---" << std::endl;
24
root->remove("Images");
25
26
std::cout << "\nDisplaying file system structure after removal:" << std::endl;
27
root->display();
28
29
return 0;
30
}
运行结果:
1
Displaying file system structure:
2
📁 Folder: Root
3
📁 Folder: Documents
4
📄 File: Report.docx (500 KB)
5
📄 File: MeetingNotes.txt (150 KB)
6
📁 Folder: Images
7
📄 File: Photo1.jpg (2000 KB)
8
📄 File: Photo2.png (1200 KB)
9
📄 File: README.md (300 KB)
10
11
--- Removing Images folder ---
12
Removed component: Images
13
14
Displaying file system structure after removal:
15
📁 Folder: Root
16
📁 Folder: Documents
17
📄 File: Report.docx (500 KB)
18
📄 File: MeetingNotes.txt (150 KB)
19
📄 File: README.md (300 KB)
在这个例子中,FileSystemComponent
是组件接口,File
是叶子,Folder
是组合。客户端可以通过 FileSystemComponent
指针统一调用 display
方法,无论是文件还是文件夹,而 Folder
的 display
方法会递归地调用其子组件的 display
方法,从而遍历整个树形结构。
4.3.4 优缺点与适用场景
优点 (Pros):
⚝ 简化客户端代码 (Simplifies Client Code): 客户端可以统一处理叶子对象和组合对象,无需区分。
⚝ 易于扩展 (Easy to Extend): 添加新的叶子或组合类型都很容易。
⚝ 定义了清晰的层次结构 (Defines Hierarchical Structures Clearly): 清晰地表达了部分-整体关系。
缺点 (Cons):
⚝ 接口设计复杂性 (Interface Design Complexity): 如果将管理子节点的方法放在 Component
接口中,叶子节点需要提供默认实现或抛出异常,这可能不是最理想的设计。
⚝ 类型安全问题 (Type Safety Issues): 在某些情况下,您可能需要在运行时检查一个 Component
是 Leaf 还是 Composite,以便执行特定操作(如添加子节点),这可以通过 dynamic_cast
实现,但增加了运行时开销和潜在的错误。
适用场景 (Applicability):
⚝ 您想表示对象的部分-整体层次结构。
⚝ 您希望用户可以忽略组合对象和单个对象之间的差异,用统一的方式来处理组合结构中的所有对象。
⚝ 您需要构建一个可以容纳对象树的结构。
4.3.5 相关模式
⚝ 装饰模式 (Decorator Pattern): 装饰模式用于动态地给对象添加职责,它维护一个指向被装饰对象的引用,并与其具有相同的接口。组合模式用于构建树形结构,组合对象包含多个子对象。虽然结构上可能有点相似(都持有一个或多个其他对象的引用),但它们的意图和应用场景是不同的。
⚝ 职责链模式 (Chain of Responsibility Pattern): 职责链模式中的处理者可以形成一个链条,请求沿着链条传递。组合模式可以用来组织处理者的层次结构,例如一个组合处理者可以将请求转发给它的所有子处理者。
4.4 装饰模式 (Decorator Pattern)
4.4.1 什么是装饰模式?
假设您正在开发一个图形用户界面 (GUI) 库中的文本框组件。文本框可能有各种附加功能,比如带滚动条、带边框、支持自动完成等等。如果您使用继承来实现这些功能,很快就会陷入类爆炸的困境:ScrollingTextBox
, BorderedTextBox
, ScrollingBorderedTextBox
, AutoCompleteScrollingBorderedTextBox
... 每增加一个功能,都需要创建大量新类。
装饰模式 (Decorator Pattern) 提供了一种更灵活的方式来动态地给一个对象添加额外的职责,而无需修改其结构。它就像给对象穿上一件件“衣服”或“配件”,每件衣服代表一种额外的功能。
意图 (Intent):动态地给一个对象添加一些额外的职责,就如同给对象戴上一个“装饰帽”。相较于使用继承,装饰者模式提供了更灵活的方式来扩展对象的功能。
装饰模式的关键在于创建一系列的装饰者 (Decorator) 类,它们包装 (wrap) 了原始对象(或另一个装饰者),并且与原始对象实现了相同的接口。每个装饰者在将其请求转发给原始对象之前或之后,可以执行自己的附加行为。
4.4.2 装饰模式的结构与参与者
装饰模式的结构包括四个主要角色:
结构图 (UML 描述):
1
classDiagram
2
direction LR
3
class Component {
4
+Operation()
5
}
6
class ConcreteComponent {
7
+Operation()
8
}
9
class Decorator {
10
+Operation()
11
#component : Component
12
}
13
class ConcreteDecoratorA {
14
+Operation()
15
}
16
class ConcreteDecoratorB {
17
+Operation()
18
}
19
20
Component <|-- ConcreteComponent
21
Component <|-- Decorator
22
Decorator o-- Component : wraps
23
Decorator <|-- ConcreteDecoratorA
24
Decorator <|-- ConcreteDecoratorB
参与者 (Participants):
⚝ 组件 (Component): 定义可以被装饰的对象的接口。在 C++ 中通常是一个抽象基类或接口。
⚝ 具体组件 (ConcreteComponent): 实现 Component
接口的原始对象,即被装饰的对象。
⚝ 装饰者 (Decorator): 继承或实现 Component
接口,并且持有一个指向 Component
对象的引用。它是所有具体装饰者的抽象基类。它的主要作用是包装原始对象,并将请求转发给它。在转发之前或之后,可以添加额外的行为。
⚝ 具体装饰者 (ConcreteDecoratorA, ConcreteDecoratorB, ...): 扩展 Decorator
类,向被包装的组件添加特定的职责。它们实现 Component
接口的方法,并在调用父类(即 Decorator
)的方法或直接调用被包装组件的方法之前或之后,执行自己的附加行为。
4.4.3 C++ 实现示例
我们来实现文本框组件的装饰。
Component
接口:Widget
,表示 GUI 组件。
1
#include <iostream>
2
#include <string>
3
#include <memory> // For std::unique_ptr
4
5
// 组件 (Component)
6
class Widget {
7
public:
8
virtual ~Widget() = default;
9
virtual void display() const = 0;
10
};
ConcreteComponent
:TextBox
,基本的文本框。
1
// 具体组件 (ConcreteComponent)
2
class TextBox : public Widget {
3
private:
4
std::string text_;
5
6
public:
7
TextBox(const std::string& text) : text_(text) {}
8
9
void display() const override {
10
std::cout << "Drawing TextBox with text: \"" << text_ << "\"" << std::endl;
11
}
12
};
Decorator
抽象类:WidgetDecorator
。
1
// 装饰者 (Decorator)
2
class WidgetDecorator : public Widget {
3
protected:
4
// 装饰者持有被包装的组件
5
std::unique_ptr<Widget> wrappedWidget_;
6
7
public:
8
WidgetDecorator(std::unique_ptr<Widget> widget) : wrappedWidget_(std::move(widget)) {}
9
virtual ~WidgetDecorator() = default;
10
11
// 转发 display 调用给被包装的组件
12
void display() const override {
13
wrappedWidget_->display();
14
}
15
};
ConcreteDecorator
:BorderDecorator
和 ScrollDecorator
,为文本框添加边框和滚动条功能。
1
// 具体装饰者 A (ConcreteDecorator A)
2
class BorderDecorator : public WidgetDecorator {
3
public:
4
BorderDecorator(std::unique_ptr<Widget> widget) : WidgetDecorator(std::move(widget)) {}
5
6
void display() const override {
7
std::cout << "Adding Border..." << std::endl;
8
WidgetDecorator::display(); // 调用父类的 display,进而调用被包装组件的 display
9
std::cout << "Border Added." << std::endl;
10
}
11
};
12
13
// 具体装饰者 B (ConcreteDecorator B)
14
class ScrollDecorator : public WidgetDecorator {
15
public:
16
ScrollDecorator(std::unique_ptr<Widget> widget) : WidgetDecorator(std::move(widget)) {}
17
18
void display() const override {
19
std::cout << "Adding Scrollbar..." << std::endl;
20
WidgetDecorator::display(); // 调用父类的 display
21
std::cout << "Scrollbar Added." << std::endl;
22
}
23
};
客户端代码 (Client) 可以自由组合这些装饰者。
1
int main() {
2
// 创建一个基本的文本框
3
std::unique_ptr<Widget> myTextBox = std::make_unique<TextBox>("Hello, Decorator!");
4
5
// 用边框装饰它
6
std::unique_ptr<Widget> borderedTextBox = std::make_unique<BorderDecorator>(std::move(myTextBox));
7
8
// 再用滚动条装饰它
9
std::unique_ptr<Widget> scrolledBorderedTextBox = std::make_unique<ScrollDecorator>(std::move(borderedTextBox));
10
11
// 显示最终的组件
12
scrolledBorderedTextBox->display();
13
14
std::cout << "\n---\n" << std::endl;
15
16
// 也可以只用滚动条装饰
17
std::unique_ptr<Widget> anotherTextBox = std::make_unique<TextBox>("Another Text");
18
std::unique_ptr<Widget> scrolledTextBox = std::make_unique<ScrollDecorator>(std::move(anotherTextBox));
19
scrolledTextBox->display();
20
21
return 0;
22
}
运行结果:
1
Adding Scrollbar...
2
Adding Border...
3
Drawing TextBox with text: "Hello, Decorator!"
4
Border Added.
5
Scrollbar Added.
6
7
---
8
9
Adding Scrollbar...
10
Drawing TextBox with text: "Another Text"
11
Scrollbar Added.
从输出可以看到,首先是 ScrollDecorator
的附加行为,然后是 BorderDecorator
的附加行为,最后是 TextBox
本身的 display
方法被调用。当从内层向外层返回时,装饰者们在其被包装组件的行为之后执行自己的附加行为(在此例中是打印 "Border Added" 和 "Scrollbar Added")。这种嵌套调用链是装饰模式的核心。
4.4.4 优缺点与适用场景
优点 (Pros):
⚝ 比继承更灵活 (More Flexible than Inheritance): 可以运行时动态地添加或移除职责,避免了继承导致的类爆炸问题。
⚝ 避免高层类拥有低层行为 (Avoids High-Level Classes Having Low-Level Behaviors): 核心功能在具体组件中实现,附加功能在装饰者中实现。
⚝ 易于添加新的装饰者 (Easy to Add New Decorators): 只需创建一个新的具体装饰者类即可。
缺点 (Cons):
⚝ 增加了代码复杂度 (Increased Complexity): 引入了多个小对象(装饰者链),调试和理解可能比单一对象复杂。
⚝ 装饰者与被装饰者难以区分 (Hard to Distinguish Decorator from Component): 客户端通过统一接口操作,无法直接获取到原始的具体组件或某个特定的装饰者(除非使用 dynamic_cast
,但这破坏了模式的透明性)。
⚝ 难以控制装饰顺序 (Difficult to Control Decoration Order): 如果装饰者之间存在依赖或顺序要求,管理起来可能比较复杂。
适用场景 (Applicability):
⚝ 需要在不影响其他对象的情况下,以透明的方式动态地给单个对象添加职责。
⚝ 不能采用继承的方式来扩展功能(例如,需要运行时添加功能,或者继承会导致类数量剧增)。
⚝ 某些类需要被扩展的功能,而这些功能又不是核心职责,可以通过独立的可选组件来实现。
4.4.5 相关模式
⚝ 适配器模式 (Adapter Pattern): 适配器改变接口,装饰者增强功能但保持接口不变。
⚝ 组合模式 (Composite Pattern): 组合模式用于构建树形结构,叶子和组合实现统一接口。装饰模式也可以形成链式结构,但其目的是增加功能,而不是表示部分-整体关系。装饰者可以被视为一种特殊的组合,它只包含一个子组件。
⚝ 策略模式 (Strategy Pattern): 策略模式用于选择不同的算法实现,通常通过组合和多态实现。装饰模式用于动态地组合功能。策略模式改变的是一个对象执行某个操作的方式(算法),而装饰模式是给对象添加新的功能或在原有功能上增强。
4.5 外观模式 (Facade Pattern)
4.5.1 什么是外观模式?
想象一下,您正在使用一个非常复杂的家庭影院系统。它可能包含蓝光播放器、音响、投影仪、调节器等等,每个设备都有自己复杂的接口和设置。如果您每次看电影都要手动操作这些设备,过程会非常繁琐。这时,一个智能遥控器 (Smart Remote) 就可以作为这个系统的“外观 (Facade)”。您只需要按一个“看电影”按钮,遥控器就会自动协调所有设备,将它们设置到正确的状态。
外观模式 (Facade Pattern) 在软件中的作用与此类似。它为子系统中的一组复杂的接口提供一个统一、简化的接口。外观隐藏了子系统的内部复杂性,使客户端更容易使用。
意图 (Intent):为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,使得子系统更容易使用。
外观模式并没有改变子系统的功能或接口,它只是在子系统之上提供了一个简化的视图。客户端通过外观与子系统交互,而无需直接访问子系统中的所有类。这样,客户端与子系统之间的耦合度降低了,同时也提高了子系统的可维护性,因为对子系统内部类的修改不会直接影响到客户端代码(除非修改了外观的接口)。
4.5.2 外观模式的结构与参与者
外观模式的结构相对简单:
结构图 (UML 描述):
1
classDiagram
2
direction LR
3
class Facade {
4
+OperationA()
5
+OperationB()
6
}
7
class SubsystemClass1 {
8
+Method1()
9
+MethodN()
10
}
11
class SubsystemClass2 {
12
+Method1()
13
+MethodZ()
14
}
15
class SubsystemClass3 {
16
+Method1()
17
+MethodQ()
18
}
19
class Client
20
21
Facade o-- SubsystemClass1
22
Facade o-- SubsystemClass2
23
Facade o-- SubsystemClass3
24
25
Client --> Facade : uses
26
Client ..> SubsystemClass1 : knows
27
Client ..> SubsystemClass2 : knows
28
Client ..> SubsystemClass3 : knows
29
30
note right of Client: Client usually only interacts\n with Facade, but can also access\n subsystem classes directly if needed.
参与者 (Participants):
⚝ 外观 (Facade): 为子系统提供统一、简化的接口。它知道子系统中的所有相关类,并将客户端的请求委派 (delegate) 给适当的子系统对象。它通常不提供新的功能,只是简化现有功能的访问。
⚝ 子系统类 (Subsystem Classes): 实现子系统功能的各种类。它们之间可能存在复杂的协作关系。外观类与这些子系统类进行交互,完成客户端请求的功能。客户端可以直接访问这些子系统类,但通过外观访问更方便。
4.5.3 C++ 实现示例
我们来实现一个简单的编译器子系统,它包括词法分析器 (Lexer)、语法分析器 (Parser)、中间代码生成器 (Code Generator) 等组件。
子系统类:
1
#include <iostream>
2
#include <string>
3
#include <vector>
4
5
// 子系统类 A (Subsystem Class A)
6
class Lexer {
7
public:
8
std::vector<std::string> tokenize(const std::string& code) {
9
std::cout << "Lexer: Tokenizing code..." << std::endl;
10
// ... actual tokenization logic
11
return {"token1", "token2"}; // Dummy tokens
12
}
13
};
14
15
// 子系统类 B (Subsystem Class B)
16
class Parser {
17
public:
18
void parse(const std::vector<std::string>& tokens) {
19
std::cout << "Parser: Parsing tokens..." << std::endl;
20
// ... actual parsing logic
21
}
22
};
23
24
// 子系统类 C (Subsystem Class C)
25
class CodeGenerator {
26
public:
27
void generate(const std::string& outputFileName) {
28
std::cout << "CodeGenerator: Generating code into " << outputFileName << std::endl;
29
// ... actual code generation logic
30
}
31
};
外观类:CompilerFacade
,提供一个简化的 compile
方法。
1
#include <iostream>
2
#include <string>
3
#include <vector>
4
#include <memory> // For std::unique_ptr
5
6
// (Previous Subsystem Classes: Lexer, Parser, CodeGenerator definitions here)
7
8
// 外观 (Facade)
9
class CompilerFacade {
10
private:
11
// 外观持有子系统类的实例
12
std::unique_ptr<Lexer> lexer_;
13
std::unique_ptr<Parser> parser_;
14
std::unique_ptr<CodeGenerator> generator_;
15
16
public:
17
CompilerFacade()
18
: lexer_(std::make_unique<Lexer>()),
19
parser_(std::make_unique<Parser>()),
20
generator_(std::make_unique<CodeGenerator>()) {}
21
22
// 提供简化的 compile 方法
23
void compile(const std::string& code, const std::string& outputFileName) {
24
std::cout << "CompilerFacade: Starting compilation..." << std::endl;
25
26
// 协调子系统类完成编译过程
27
std::vector<std::string> tokens = lexer_->tokenize(code);
28
parser_->parse(tokens);
29
generator_->generate(outputFileName);
30
31
std::cout << "CompilerFacade: Compilation finished." << std::endl;
32
}
33
};
客户端代码 (Client) 只需与 CompilerFacade
交互。
1
int main() {
2
// 客户端使用外观类,无需直接与 Lexer, Parser, CodeGenerator 交互
3
CompilerFacade compiler;
4
std::string sourceCode = "int main() { return 0; }";
5
std::string output = "program.exe";
6
7
std::cout << "Client: Compiling source code..." << std::endl;
8
compiler.compile(sourceCode, output);
9
std::cout << "Client: Compilation request sent." << std::endl;
10
11
return 0;
12
}
运行结果:
1
Client: Compiling source code...
2
CompilerFacade: Starting compilation...
3
Lexer: Tokenizing code...
4
Parser: Parsing tokens...
5
CodeGenerator: Generating code into program.exe
6
CompilerFacade: Compilation finished.
7
Client: Compilation request sent.
在这个例子中,CompilerFacade
隐藏了编译过程的复杂性(词法分析、语法分析、代码生成),为客户端提供了一个简单的 compile
方法。客户端调用这个方法就可以完成整个编译过程,而无需了解 Lexer
, Parser
, CodeGenerator
等类的存在和它们之间的协作关系。
4.5.4 优缺点与适用场景
优点 (Pros):
⚝ 简化客户端接口 (Simplifies Client Interface): 为复杂子系统提供一个简单的入口点。
⚝ 降低客户端与子系统耦合度 (Reduces Coupling): 客户端只与外观交互,而不是直接与子系统中的多个类耦合。
⚝ 提高了子系统的独立性 (Improves Subsystem Independence): 子系统内部的变化对外层客户端影响较小,因为客户端依赖的是外观接口。
⚝ 更容易分层 (Easier Layering): 可以利用外观模式来为系统创建不同的层,例如在应用程序层和中间件层之间引入外观。
缺点 (Cons):
⚝ 可能变成“上帝对象” (Potential "God Object"): 如果外观承担了过多职责,它可能会变得过于庞大和复杂,难以维护。
⚝ 隐藏了灵活性 (Hides Flexibility): 有些客户端可能需要更细粒度的控制,而外观提供的接口可能不够灵活。但这通常是可以接受的权衡,因为它针对的是大多数简单用例。
适用场景 (Applicability):
⚝ 当您想为一个复杂子系统提供一个简单的接口时。
⚝ 客户端程序与多个子系统类之间存在很多依赖关系时,可以使用外观来降低耦合。
⚝ 当您想对子系统进行分层时,可以使用外观模式作为每一层的入口。
⚝ 当子系统不断演化,新的类和接口不断增加,导致客户端难以使用时。
4.5.5 相关模式
⚝ 抽象工厂模式 (Abstract Factory Pattern): 抽象工厂提供一个接口来创建一系列相关或相互依赖的对象。外观模式为一组现有对象提供一个简化的接口。它们都涉及创建一个更简单的接口来隐藏复杂性,但侧重点不同。抽象工厂侧重于对象的创建,而外观侧重于对象的使用。
⚝ 中介者模式 (Mediator Pattern): 中介者模式用于协调一组对象之间的交互,这些对象之间的交互方式被封装在中介者对象中。外观模式是简化一个子系统的接口,而中介者模式是管理多个对象之间的复杂通信。
⚝ 单例模式 (Singleton Pattern): 外观类本身通常可以设计成单例,因为它通常只需要一个实例来提供子系统的访问入口。
⚝ 适配器模式 (Adapter Pattern): 适配器模式用于转换一个类的接口以匹配另一个接口。外观模式用于为整个子系统提供一个简化的接口。一个外观类可能在内部使用适配器来处理子系统中的不兼容接口。
4.6 享元模式 (Flyweight Pattern)
4.6.1 什么是享元模式?
想象一个文字编辑器,里面可能有成千上万个字符。如果每个字符(包括字体、颜色、大小等格式信息)都作为一个独立的对象,那么内存开销将非常巨大。但是,大多数字符的格式信息是相同的,只有它们的位置和具体字符不同。
享元模式 (Flyweight Pattern) 是一种用于有效地支持大量细粒度对象的复用技术。它通过共享对象中不可变的状态 (Intrinsic State) 来减少内存消耗,同时将可变的状态 (Extrinsic State) 外部化。不可变状态是对象固有的、可以共享的状态,而可变状态是对象随着环境变化而变化、不能共享的状态。
意图 (Intent):运用共享技术有效地支持大量细粒度对象的复用。
享元模式将对象的属性分为两类:
① 内部状态 (Intrinsic State): 存储在享元对象内部,可以被共享,并且不受外部环境影响而改变。
② 外部状态 (Extrinsic State): 随客户端环境变化而变化,不能共享,由客户端或调用享元的方法时传递。
享元工厂 (Flyweight Factory) 负责管理享元对象的创建和共享。当客户端请求一个享元对象时,工厂会先检查是否已存在具有相同内部状态的对象。如果存在,则返回现有对象;如果不存在,则创建一个新的对象并将其存储起来供以后共享。
4.6.2 享元模式的结构与参与者
享元模式的结构包括四个主要角色:
结构图 (UML 描述):
1
classDiagram
2
direction LR
3
class Flyweight {
4
+Operation(extrinsicState)
5
#intrinsicState
6
}
7
class ConcreteFlyweight {
8
+Operation(extrinsicState)
9
}
10
class UnsharedConcreteFlyweight {
11
+Operation(extrinsicState)
12
}
13
class FlyweightFactory {
14
+GetFlyweight(key)
15
}
16
class Client {
17
-flyweights : Flyweight[]
18
-extrinsicState : State[]
19
}
20
21
Flyweight <|-- ConcreteFlyweight
22
Flyweight <|-- UnsharedConcreteFlyweight
23
FlyweightFactory --> Flyweight : creates/manages
24
Client --> FlyweightFactory : requests
25
Client "1" *--> "*" Flyweight : uses
参与者 (Participants):
⚝ 享元 (Flyweight): 定义了享元的接口,并通过这个接口使享元可以接受并作用于外部状态。在 C++ 中通常是一个抽象基类或接口。
⚝ 具体享元 (ConcreteFlyweight): 实现了 Flyweight
接口,存储内部状态。所有具有相同内部状态的对象都共享同一个 ConcreteFlyweight
实例。
⚝ 非共享具体享元 (UnsharedConcreteFlyweight): 表示那些不能被共享的 Flyweight
子类。它们可能包含只对自身对象有效的状态。在某些情况下可能会用到(例如,组合模式中的组合节点),但这不是享元模式的核心。GoF 书中将其视为可选部分。
⚝ 享元工厂 (FlyweightFactory): 创建并管理 Flyweight
对象。它维护一个享元池,当客户端请求一个享元时,检查池中是否已存在,如果存在则返回,否则创建新的并加入池。
⚝ 客户端 (Client): 维护对享元的引用,并存储或计算外部状态。在调用享元对象的方法时,将外部状态传递给享元。
4.6.3 C++ 实现示例
我们以字符格式为例来实现享元模式。
内部状态:字体、颜色、大小。外部状态:字符本身的值、位置。
Flyweight
接口:CharacterFormat
,表示字符的格式。
1
#include <iostream>
2
#include <string>
3
#include <map>
4
#include <memory>
5
6
// 享元 (Flyweight)
7
class CharacterFormat {
8
private:
9
// 内部状态 (Intrinsic State): 字体、颜色、大小
10
std::string font_;
11
std::string color_;
12
int size_;
13
14
public:
15
// 构造函数私有,只能由享元工厂创建
16
CharacterFormat(const std::string& font, const std::string& color, int size)
17
: font_(font), color_(color), size_(size) {
18
std::cout << "Creating CharacterFormat: " << font_ << ", " << color_ << ", " << size_ << std::endl;
19
}
20
21
// Operation 方法接受外部状态 (Extrinsic State)
22
void display(char character, int x, int y) const {
23
std::cout << "Displaying character '" << character << "' at (" << x << ", " << y
24
<< ") with format [Font: " << font_ << ", Color: " << color_ << ", Size: " << size_ << "]" << std::endl;
25
}
26
27
// Getter for intrinsic state (optional, primarily for debugging/factory use)
28
const std::string& getFont() const { return font_; }
29
const std::string& getColor() const { return color_; }
30
int getSize() const { return size_; }
31
};
FlyweightFactory
: CharacterFormatFactory
,管理 CharacterFormat
对象的缓存。
1
// 享元工厂 (Flyweight Factory)
2
class CharacterFormatFactory {
3
private:
4
// 享元池 (Flyweight Pool)
5
std::map<std::string, std::shared_ptr<CharacterFormat>> pool_;
6
7
// 用于生成缓存键
8
std::string getKey(const std::string& font, const std::string& color, int size) const {
9
return font + "_" + color + "_" + std::to_string(size);
10
}
11
12
public:
13
// 获取或创建享元对象
14
std::shared_ptr<CharacterFormat> getCharacterFormat(const std::string& font, const std::string& color, int size) {
15
std::string key = getKey(font, color, size);
16
17
if (pool_.find(key) == pool_.end()) {
18
// 如果池中不存在,则创建新的享元对象并加入池中
19
pool_[key] = std::make_shared<CharacterFormat>(font, color, size);
20
}
21
// 返回池中已有的或新创建的对象
22
return pool_[key];
23
}
24
25
size_t getPoolSize() const {
26
return pool_.size();
27
}
28
};
客户端代码 (Client) 使用享元工厂获取享元对象,并在调用其方法时传递外部状态。
1
int main() {
2
CharacterFormatFactory factory;
3
4
// 模拟在文本编辑器中显示一些字符
5
// 外部状态:字符本身 ('H', 'e', 'l', ...),位置 (x, y)
6
// 内部状态:格式 (Arial, Black, 12)
7
8
std::string text = "Hello World";
9
10
std::cout << "Displaying text: \"" << text << "\"" << std::endl;
11
12
// 使用相同的格式显示所有字符
13
std::shared_ptr<CharacterFormat> commonFormat = factory.getCharacterFormat("Arial", "Black", 12);
14
15
for (int i = 0; i < text.length(); ++i) {
16
// 对于每个字符,使用共享的享元对象,并传递当前的字符和位置作为外部状态
17
commonFormat->display(text[i], i * 10, 50); // Example position calculation
18
}
19
20
std::cout << "\n---\n" << std::endl;
21
22
// 模拟改变一部分文本的格式
23
std::string highlightedText = "Flyweight Pattern";
24
std::shared_ptr<CharacterFormat> normalFormat = factory.getCharacterFormat("Arial", "Black", 12); // 从池中获取已有的
25
std::shared_ptr<CharacterFormat> highlightedFormat = factory.getCharacterFormat("Arial", "Red", 14); // 创建新的格式
26
27
for (int i = 0; i < highlightedText.length(); ++i) {
28
if (i < 10) { // 前10个字符使用普通格式
29
normalFormat->display(highlightedText[i], i * 10, 100);
30
} else { // 后面的字符使用高亮格式
31
highlightedFormat->display(highlightedText[i], i * 10, 100);
32
}
33
}
34
35
std::cout << "\n---\n" << std::endl;
36
std::cout << "Total unique character formats created (pool size): " << factory.getPoolSize() << std::endl;
37
38
return 0;
39
}
运行结果:
1
Creating CharacterFormat: Arial, Black, 12
2
Displaying character 'H' at (0, 50) with format [Font: Arial, Color: Black, Size: 12]
3
Displaying character 'e' at (10, 50) with format [Font: Arial, Color: Black, Size: 12]
4
... (repeated for all characters in "Hello World")
5
Displaying character 'd' at (100, 50) with format [Font: Arial, Color: Black, Size: 12]
6
7
---
8
9
Displaying character 'F' at (0, 100) with format [Font: Arial, Color: Black, Size: 12]
10
Displaying character 'l' at (10, 100) with format [Font: Arial, Color: Black, Size: 12]
11
... (for "Flyweight ")
12
Displaying character ' ' at (90, 100) with format [Font: Arial, Color: Black, Size: 12]
13
Creating CharacterFormat: Arial, Red, 14
14
Displaying character 'P' at (100, 100) with format [Font: Arial, Red, 14]
15
Displaying character 'a' at (110, 100) with format [Font: Arial, Red, 14]
16
... (for "Pattern")
17
18
---
19
20
Total unique character formats created (pool size): 2
从输出可以看到,尽管我们显示了多个字符,但只创建了两种不同的 CharacterFormat
对象(一种是 Arial, Black, 12,另一种是 Arial, Red, 14)。所有使用相同格式的字符都共享同一个 CharacterFormat
实例,并通过传递不同的外部状态(字符本身和位置)来显示。这在处理大量具有相同内部状态的对象时能显著节省内存。
4.6.4 优缺点与适用场景
优点 (Pros):
⚝ 显著减少内存消耗 (Significantly Reduces Memory Consumption): 通过共享对象,特别是当对象数量巨大且大部分状态可以外部化时,效果非常明显。
⚝ 提高性能 (Potentially Improves Performance): 减少了对象创建和垃圾回收的开销(尽管 C++ 没有垃圾回收,但减少 new
和 delete
操作仍然有益)。
缺点 (Cons):
⚝ 增加了系统复杂性 (Increased Complexity): 需要区分和管理内部状态和外部状态,并引入享元工厂,设计和实现起来更复杂。
⚝ 运行时开销 (Runtime Overhead): 每次调用享元方法时都需要传递外部状态,并可能涉及在工厂中查找享元对象(如果工厂没有优化查找)。
⚝ 将状态外部化可能不容易 (Externalizing State Can Be Difficult): 识别哪些状态是内部的、哪些是外部的,并确保外部状态能被正确地管理和传递,需要仔细设计。
适用场景 (Applicability):
⚝ 系统中存在大量对象。
⚝ 这些对象的大多数状态可以被外部化。
⚝ 经过外部化后,可以由少数共享对象取代大量对象。
⚝ 应用场景对内存消耗有严格要求。
4.6.5 相关模式
⚝ 工厂模式 (Factory Pattern): 享元工厂 (FlyweightFactory
) 在享元模式中扮演了工厂的角色,负责创建和管理享元对象。
⚝ 状态模式 (State Pattern): 状态模式中的状态对象有时可以被实现为享元,特别是当状态对象数量较多且其内部状态可以共享时。
⚝ 组合模式 (Composite Pattern): 组合模式中的叶子节点可以被实现为享元,以处理大量相似的叶子对象。组合节点通常不能被共享,可以被视为非共享具体享元。
4.7 代理模式 (Proxy Pattern)
4.7.1 什么是代理模式?
设想您需要访问一个非常大的图像文件。加载整个图像到内存可能需要很长时间,或者您可能根本不需要在开始时就完全加载它,只需要显示一个缩略图或者在用户真正滚动到图像位置时才加载。这时,您可以使用一个代理 (Proxy) 对象来代表这个图像对象。客户端与代理交互,代理控制对真实图像对象的访问,并在必要时才创建和加载真实对象。
代理模式 (Proxy Pattern) 为另一个对象提供一个替身或占位符,以控制对这个对象的访问。这个替身对象就是代理。代理和被代理的真实对象实现了相同的接口,因此客户端可以在不知道自己操作的是代理还是真实对象的情况下进行编程。
意图 (Intent):为另一个对象提供一个替身或占位符以控制对这个对象的访问。
代理模式可以用于多种目的:
⚝ 远程代理 (Remote Proxy): 代表位于不同地址空间的对象(如 RPC 中的客户端存根)。
⚝ 虚拟代理 (Virtual Proxy): 根据需要延迟对象的创建和初始化(如延迟加载大图像或昂贵的对象)。
⚝ 保护代理 (Protection Proxy): 控制对原始对象的访问权限(如根据用户权限决定是否允许调用某个方法)。
⚝ 智能引用 (Smart Reference): 取代普通的指针,在访问对象时执行额外操作,如引用计数、延迟加载、锁定等(C++ 中的智能指针 std::shared_ptr
, std::unique_ptr
可以看作是一种智能引用代理)。
4.7.2 代理模式的结构与参与者
代理模式的结构包括三个主要角色:
结构图 (UML 描述):
1
classDiagram
2
direction LR
3
class Subject {
4
+Request()
5
}
6
class RealSubject {
7
+Request()
8
}
9
class Proxy {
10
+Request()
11
-realSubject : RealSubject
12
}
13
class Client
14
15
Subject <|-- RealSubject
16
Subject <|-- Proxy
17
Proxy o-- RealSubject : holds reference to
18
19
Client --> Subject : uses
20
Client --> Proxy : usually interacts via Proxy
参与者 (Participants):
⚝ 主体 (Subject): 定义 RealSubject
和 Proxy
的共同接口,这样在任何可以使用 RealSubject
的地方都可以使用 Proxy
。在 C++ 中通常是一个抽象基类或接口。
⚝ 真实主体 (RealSubject): 代理所代表的真实对象。它实现了 Subject
接口,包含了核心业务逻辑。
⚝ 代理 (Proxy): 维护一个引用,指向 RealSubject
对象。它与 RealSubject
实现了相同的 Subject
接口,并控制对 RealSubject
的访问。在访问 RealSubject
之前或之后,代理可以执行附加操作。
4.7.3 C++ 实现示例
我们以虚拟代理为例,实现一个延迟加载的大图像对象。
Subject
接口:Image
。
1
#include <iostream>
2
#include <string>
3
#include <memory>
4
5
// 主体 (Subject)
6
class Image {
7
public:
8
virtual ~Image() = default;
9
virtual void display() const = 0;
10
};
RealSubject
: RealImage
,表示真实的图像加载和显示逻辑。
1
// 真实主体 (RealSubject)
2
class RealImage : public Image {
3
private:
4
std::string filename_;
5
6
// 模拟加载图像的耗时操作
7
void loadImage() const {
8
std::cout << " Loading image from " << filename_ << "..." << std::endl;
9
// ... actual loading logic (e.g., reading from disk, decoding)
10
// Simulate delay
11
// std::this_thread::sleep_for(std::chrono::seconds(2));
12
std::cout << " Image loaded." << std::endl;
13
}
14
15
public:
16
RealImage(const std::string& filename) : filename_(filename) {
17
// RealImage 的构造函数不加载图像,加载在需要时进行
18
std::cout << "RealImage object created for " << filename_ << std::endl;
19
}
20
21
~RealImage() {
22
std::cout << "RealImage object for " << filename_ << " destroyed." << std::endl;
23
}
24
25
void display() const override {
26
// 在第一次 display 时加载图像
27
// 注意:如果 loadImage 需要修改对象状态(如设置一个标志表明已加载),
28
// display 方法就不应该是 const 的,或者加载逻辑需要放在一个非 const 方法中。
29
// 简单起见,这里假设加载是模拟的且不修改状态。
30
// 更健壮的设计会使用一个 mutable 成员或在非 const 方法中处理加载状态。
31
// 更好的做法是Proxy决定何时加载,而不是RealImage的const方法。
32
// 我们将在 Proxy 中控制加载。
33
std::cout << " Displaying image " << filename_ << std::endl;
34
// ... actual display logic
35
}
36
37
const std::string& getFilename() const { return filename_; }
38
};
Proxy
: ImageProxy
,作为 RealImage
的代理。
1
// 代理 (Proxy)
2
class ImageProxy : public Image {
3
private:
4
// 代理持有一个指向真实对象的指针
5
mutable std::unique_ptr<RealImage> realImage_; // mutable allows modification in const methods (e.g., lazy loading)
6
std::string filename_;
7
8
public:
9
ImageProxy(const std::string& filename) : filename_(filename) {
10
std::cout << "ImageProxy object created for " << filename_ << std::endl;
11
// 代理的构造函数不创建 RealImage 对象,延迟到需要时
12
}
13
14
~ImageProxy() {
15
std::cout << "ImageProxy object for " << filename_ << " destroyed." << std::endl;
16
// unique_ptr automatically destroys realImage_ if it was created
17
}
18
19
void display() const override {
20
std::cout << "ImageProxy::display() called for " << filename_ << std::endl;
21
// 在第一次调用 display() 时才创建并加载 RealImage 对象 (延迟加载)
22
if (!realImage_) {
23
std::cout << " Proxy: Real image not loaded yet. Loading now..." << std::endl;
24
realImage_ = std::make_unique<RealImage>(filename_);
25
// 为了模拟加载,在这里调用 RealImage 的加载逻辑 (如果 RealImage 有独立的加载方法)
26
// realImage_->loadImage(); // 如果 RealImage 有此方法
27
}
28
// 将请求转发给真实对象
29
realImage_->display();
30
}
31
32
// 可以添加其他代理特有的方法,例如获取文件名、显示缩略图等
33
const std::string& getFilename() const { return filename_; }
34
void displayThumbnail() const {
35
std::cout << "Displaying thumbnail for " << filename_ << " (no loading needed)." << std::endl;
36
// ... display thumbnail logic
37
}
38
};
客户端代码 (Client) 通过 Image
接口与 ImageProxy
交互。
1
int main() {
2
// 客户端使用 Image 接口,创建 ImageProxy 对象
3
std::unique_ptr<Image> image1 = std::make_unique<ImageProxy>("large_photo_1.jpg");
4
std::unique_ptr<Image> image2 = std::make_unique<ImageProxy>("large_photo_2.png");
5
6
std::cout << "\n--- After creating proxies ---\n" << std::endl;
7
// 注意:此时 RealImage 对象还没有被创建
8
9
// 第一次调用 display(),会触发 RealImage 的创建和加载
10
std::cout << "Client: Calling display() on image1..." << std::endl;
11
image1->display();
12
13
std::cout << "\n--- After first display ---\n" << std::endl;
14
// RealImage for image1 is now loaded
15
16
// 第二次调用 display(),直接使用已创建的 RealImage
17
std::cout << "Client: Calling display() again on image1..." << std::endl;
18
image1->display();
19
20
std::cout << "\n--- After second display ---\n" << std::endl;
21
22
// 调用 display() on image2,会触发 RealImage for image2 的创建和加载
23
std::cout << "Client: Calling display() on image2..." << std::endl;
24
image2->display();
25
26
std::cout << "\n--- After display on image2 ---\n" << std::endl;
27
28
29
// 使用代理特有的方法 (如果需要)
30
// 注意:如果需要调用代理特有的方法,客户端可能需要知道它是 ImageProxy
31
// 这可能会破坏透明性,取决于具体需求。
32
// 例如: dynamic_cast<ImageProxy*>(image1.get())->displayThumbnail();
33
34
35
return 0; // image1 和 image2 (以及内部的 realImage_) 会在这里自动销毁
36
}
运行结果:
1
ImageProxy object created for large_photo_1.jpg
2
ImageProxy object created for large_photo_2.png
3
4
--- After creating proxies ---
5
6
Client: Calling display() on image1...
7
ImageProxy::display() called for large_photo_1.jpg
8
Proxy: Real image not loaded yet. Loading now...
9
RealImage object created for large_photo_1.jpg
10
Loading image from large_photo_1.jpg...
11
Image loaded.
12
Displaying image large_photo_1.jpg
13
14
--- After first display ---
15
16
Client: Calling display() again on image1...
17
ImageProxy::display() called for large_photo_1.jpg
18
Displaying image large_photo_1.jpg
19
20
--- After second display ---
21
22
Client: Calling display() on image2...
23
ImageProxy::display() called for large_photo_2.png
24
Proxy: Real image not loaded yet. Loading now...
25
RealImage object created for large_photo_2.png
26
Loading image from large_photo_2.png...
27
Image loaded.
28
Displaying image large_photo_2.png
29
30
--- After display on image2 ---
31
32
RealImage object for large_photo_1.jpg destroyed.
33
RealImage object for large_photo_2.png destroyed.
34
ImageProxy object for large_photo_1.jpg destroyed.
35
ImageProxy object for large_photo_2.png destroyed.
从输出可以看出,RealImage
对象直到其 display()
方法被第一次调用时才被创建(延迟加载)。ImageProxy
控制了这个创建过程,并在后续调用中直接将请求转发给已创建的 RealImage
对象。这在处理大型或昂贵的资源时非常有用。
4.7.4 优缺点与适用场景
优点 (Pros):
⚝ 控制访问 (Controls Access): 代理可以在客户端和真实对象之间插入一层,控制何时、如何以及是否允许访问真实对象。
⚝ 延迟开销 (Defers Costs): 虚拟代理可以延迟昂贵对象的创建,直到真正需要时才进行。
⚝ 提高了性能 (Potentially Improves Performance): 例如,远程代理可以缓存结果,或者虚拟代理避免不必要的对象创建。
⚝ 增强了对象功能 (Enhances Object Functionality): 可以在不修改真实对象的情况下添加额外的功能(如日志记录、访问控制、同步等)。
缺点 (Cons):
⚝ 增加了代码复杂度 (Increased Complexity): 引入了新的代理类,增加了系统的类数量。
⚝ 可能引入延迟 (Potential Latency): 代理在转发请求时可能会引入额外的处理或网络延迟(对于远程代理)。
⚝ 客户端需要了解代理 (Client May Need to Be Aware of Proxy): 如果代理提供了真实对象没有的额外接口(例如 displayThumbnail()
),客户端可能需要区分代理和真实对象,这会破坏透明性。
适用场景 (Applicability):
⚝ 远程代理: 需要访问位于不同地址空间的对象时。
⚝ 虚拟代理: 需要创建昂贵对象时,希望延迟其创建直到真正需要使用时。
⚝ 保护代理: 需要根据权限控制对对象的访问时。
⚝ 智能引用: 需要在访问对象时执行附加操作,如引用计数、锁定、延迟加载等(C++ 智能指针是典型例子)。
4.7.5 相关模式
⚝ 适配器模式 (Adapter Pattern): 适配器改变被包装对象的接口以匹配目标接口,而代理提供相同的接口并控制对真实对象的访问。
⚝ 装饰模式 (Decorator Pattern): 装饰者和代理都包装了另一个对象并与其具有相同的接口。装饰者主要目的是动态地添加或增强功能;代理主要目的是控制访问或管理对象的生命周期/创建。装饰者链可以嵌套,而代理通常只包装一个真实对象。
⚝ 外观模式 (Facade Pattern): 外观为整个子系统提供一个简化的接口,而代理是为单个对象提供一个替身以控制访问。外观通常隐藏了子系统的多个对象,而代理通常只代表一个真实对象。
⚝ 享元模式 (Flyweight Pattern): 享元模式通过共享对象来节省内存,而代理模式通过延迟加载或控制访问来管理对象的生命周期和行为。有时,虚拟代理可能与享元结合使用,例如为每个享元对象提供一个代理,以便在第一次访问时加载其复杂的内部状态。
好的,关于结构型模式的讲解就到这里。希望通过这些详细的解析和 C++ 示例,您能对这些模式的原理、应用和实现有更深入的理解。理解了如何巧妙地组织类与对象,您就能设计出更灵活、更健壮、更易于维护的 C++ 软件系统。
在下一章,我们将转向行为型模式,探讨对象之间的算法和职责分配,以及它们如何相互协作以完成任务。敬请期待!👍
5. 行为型模式:交互的智慧
欢迎来到本书的第五章,我们将深入探讨行为型设计模式 (Behavioral Patterns)。如果说创建型模式 (Creational Patterns) 关注对象的产生方式,结构型模式 (Structural Patterns) 关注类和对象的组合结构,那么行为型模式则聚焦于对象之间的算法和职责分配,以及它们如何相互协作以完成共同的任务。
行为型模式描述了对象或类如何相互协作以完成单个对象无法完成的任务。它们涉及算法、职责、通信等方面,提供了更加灵活和可维护的对象交互方式。理解和掌握行为型模式,能够帮助我们设计出职责清晰、耦合松散、易于扩展的软件系统。
本章将详细解析GoF (Gang of Four) 书中定义的全部行为型模式,包括它们的意图、动机、结构、参与者、协作方式、优缺点以及在C++语言中的具体实现和应用。我们还将结合现代C++的特性,探讨如何以更现代、更高效的方式来实现这些模式。希望通过本章的学习,您能更好地理解对象间的动态行为,提升设计复杂系统的能力。
5.1 责任链模式 (Chain of Responsibility Pattern)
意图 (Intent)
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
动机 (Motivation)
假设您正在设计一个事件处理系统,例如GUI应用程序中的鼠标点击或键盘输入,或者是一个审批流程系统。请求(事件)可能由多种不同类型的处理器来处理,但发送者不知道具体哪个处理器能处理这个请求,也不知道哪些处理器可能对这个请求感兴趣。如果发送者直接与所有可能的接收者耦合,那么当接收者集合发生变化时,发送者代码就需要修改,这违反了开放/封闭原则 (Open/Closed Principle)。
责任链模式提供了一种解决方案:将处理请求的对象组织成一条链。当一个请求到达时,它会沿着这条链依次传递。链上的每个对象都有机会处理请求。如果它能处理,就处理并可能终止传递;如果不能或不想处理,就将请求传递给链中的下一个对象。这样,请求的发送者只需要知道链的起点,而不需要知道链上的具体处理器,从而实现了发送者和接收者之间的解耦。
结构 (Structure)
模式的关键参与者包括:
⚝ Handler (处理者):
▮▮▮▮⚝ 定义一个处理请求的接口。
▮▮▮▮⚝ 实现后继者 (successor) 的链接。
▮▮▮▮⚝ 提供处理请求的方法,以及设置/获取后继者的方法。
⚝ ConcreteHandler (具体处理者):
▮▮▮▮⚝ 实现Handler接口。
▮▮▮▮⚝ 处理它负责的请求。如果不能处理,就将请求传递给它的后继者。
⚝ Client (客户端):
▮▮▮▮⚝ 创建处理者对象并组织成链。
▮▮▮▮⚝ 向链的头部发送请求。
一个简化的UML类图结构(文本描述):
1
graph LR
2
Client --> Handler
3
Handler <|-- ConcreteHandlerA
4
Handler <|-- ConcreteHandlerB
5
Handler --|> Handler: successor
协作 (Collaboration)
① 当一个请求到达链的头部(第一个ConcreteHandler)时,该处理者首先判断自己是否能处理这个请求。
② 如果能处理,它会执行相应的处理逻辑。这可能意味着请求处理结束,也可能意味着处理后还需要继续传递(取决于具体实现)。
③ 如果不能处理(或者需要继续传递),它会将请求转发给其后继者 (successor)。
④ 这个过程会沿着链条重复,直到请求被某个ConcreteHandler处理,或者到达链的末端仍未被处理。
优点 (Advantages)
⚝ 降低耦合度 (Decoupling): 发送者和接收者之间完全解耦。发送者只需向链的头部发送请求,无需知道具体的处理者。
⚝ 增强了给对象指派职责的灵活性 (Increased Flexibility): 可以在运行时动态地增加、删除或重排链中的处理者,从而改变处理请求的方式。
⚝ 简化了对象之间的连接 (Simplifies Connections): 每个处理者只需要知道其后继者,而不需要知道链上的所有其他处理者。
缺点 (Disadvantages)
⚝ 请求可能未被处理 (Request May Not Be Handled): 如果链中没有处理者能够处理特定的请求,请求可能会一直传递到链的末端而未被处理。需要额外的逻辑来处理这种情况(例如,在链的末端添加一个默认处理者)。
⚝ 性能问题 (Performance Issues): 请求必须沿着链传递,直到找到合适的处理者。如果链很长,或者请求处理者位于链的末端,可能会导致性能开销。
⚝ 可能导致循环 (Potential for Loops): 在构建链时,如果设置后继者不当,可能会导致请求在链中循环传递。
C++实现示例 (C++ Implementation Example)
假设我们有一个请假审批系统,审批流程为:组长 -> 部门经理 -> 总经理。
1
#include <iostream>
2
#include <string>
3
#include <memory> // For std::shared_ptr
4
5
// 定义请求类
6
class LeaveRequest {
7
public:
8
LeaveRequest(const std::string& name, int days) :
9
m_employeeName(name), m_leaveDays(days) {}
10
11
const std::string& getEmployeeName() const { return m_employeeName; }
12
int getLeaveDays() const { return m_leaveDays; }
13
14
private:
15
std::string m_employeeName;
16
int m_leaveDays;
17
};
18
19
// 抽象处理者 (Handler)
20
class Approver {
21
public:
22
Approver(const std::string& name) : m_name(name) {}
23
24
// 设置后继者
25
void setSuccessor(std::shared_ptr<Approver> successor) {
26
m_successor = successor;
27
}
28
29
// 处理请求的抽象方法
30
virtual void processRequest(const LeaveRequest& request) = 0;
31
32
protected:
33
std::string m_name; // 审批者姓名
34
std::shared_ptr<Approver> m_successor; // 后继审批者
35
};
36
37
// 具体处理者:组长 (ConcreteHandler)
38
class TeamLeader : public Approver {
39
public:
40
TeamLeader(const std::string& name) : Approver(name) {}
41
42
void processRequest(const LeaveRequest& request) override {
43
if (request.getLeaveDays() <= 1) {
44
std::cout << m_name << " (组长) 批准了 "
45
<< request.getEmployeeName() << " 的 "
46
<< request.getLeaveDays() << " 天请假申请。" << std::endl;
47
} else if (m_successor) {
48
std::cout << m_name << " (组长) 无权批准超过1天的请假,将请求转发给上级..." << std::endl;
49
m_successor->processRequest(request);
50
} else {
51
std::cout << m_name << " (组长) 无权批准超过1天的请假,且没有上级处理。" << std::endl;
52
}
53
}
54
};
55
56
// 具体处理者:部门经理 (ConcreteHandler)
57
class DepartmentManager : public Approver {
58
public:
59
DepartmentManager(const std::string& name) : Approver(name) {}
60
61
void processRequest(const LeaveRequest& request) override {
62
if (request.getLeaveDays() <= 3) {
63
std::cout << m_name << " (部门经理) 批准了 "
64
<< request.getEmployeeName() << " 的 "
65
<< request.getLeaveDays() << " 天请假申请。" << std::endl;
66
} else if (m_successor) {
67
std::cout << m_name << " (部门经理) 无权批准超过3天的请假,将请求转发给上级..." << std::endl;
68
m_successor->processRequest(request);
69
} else {
70
std::cout << m_name << " (部门经理) 无权批准超过3天的请假,且没有上级处理。" << std::endl;
71
}
72
}
73
};
74
75
// 具体处理者:总经理 (ConcreteHandler)
76
class GeneralManager : public Approver {
77
public:
78
GeneralManager(const std::string& name) : Approver(name) {}
79
80
void processRequest(const LeaveRequest& request) override {
81
if (request.getLeaveDays() <= 7) { // 假设总经理最多批准7天
82
std::cout << m_name << " (总经理) 批准了 "
83
<< request.getEmployeeName() << " 的 "
84
<< request.getLeaveDays() << " 天请假申请。" << std::endl;
85
} else if (m_successor) { // 总经理通常是链的最后一环,但保留扩展性
86
std::cout << m_name << " (总经理) 无权批准超过7天的请假,将请求转发给上级..." << std::endl;
87
m_successor->processRequest(request);
88
}
89
else {
90
std::cout << m_name << " (总经理) 无权批准超过7天的请假,审批流程结束,申请被拒绝。" << std::endl;
91
}
92
}
93
};
94
95
// 客户端代码 (Client)
96
int main() {
97
// 构建责任链
98
auto leader = std::make_shared<TeamLeader>("张三");
99
auto manager = std::make_shared<DepartmentManager>("李四");
100
auto general = std::make_shared<GeneralManager>("王五");
101
102
leader->setSuccessor(manager);
103
manager->setSuccessor(general);
104
// general->setSuccessor(nullptr); // 总经理是链的末端,不需要后继者或显式设为nullptr
105
106
// 发送请求
107
LeaveRequest req1("小明", 1);
108
std::cout << "发送请假申请: " << req1.getEmployeeName() << " 请假 " << req1.getLeaveDays() << " 天" << std::endl;
109
leader->processRequest(req1);
110
std::cout << "--------------------" << std::endl;
111
112
LeaveRequest req2("小红", 3);
113
std::cout << "发送请假申请: " << req2.getEmployeeName() << " 请假 " << req2.getLeaveDays() << " 天" << std::endl;
114
leader->processRequest(req2);
115
std::cout << "--------------------" << std::endl;
116
117
LeaveRequest req3("小刚", 5);
118
std::cout << "发送请假申请: " << req3.getEmployeeName() << " 请假 " << req3.getLeaveDays() << " 天" << std::endl;
119
leader->processRequest(req3);
120
std::cout << "--------------------" << std::endl;
121
122
LeaveRequest req4("小强", 10);
123
std::cout << "发送请假申请: " << req4.getEmployeeName() << " 请假 " << req4.getLeaveDays() << " 天" << std::endl;
124
leader->processRequest(req4);
125
std::cout << "--------------------" << std::endl;
126
127
return 0;
128
}
在C++实现中,我们通常使用抽象基类来定义Handler接口,并使用虚函数来实现多态。后继者的链接可以使用指针或智能指针 (Smart Pointers),这里使用了std::shared_ptr
来管理对象的生命周期。每个具体处理者在其processRequest
方法中实现自己的处理逻辑,并根据需要将请求传递给后继者。客户端负责构建链并启动请求。
适用性 (Applicability)
责任链模式适用于以下情况:
⚝ 有多个对象可以处理一个请求,但具体由哪个对象处理在运行时才能确定。
⚝ 想在不明确指定接收者的情况下,向多个对象中的一个提交请求。
⚝ 希望动态指定处理请求的对象集合,或者希望改变它们之间的处理顺序。
相关模式 (Related Patterns)
⚝ 命令模式 (Command Pattern): 责任链模式可以与命令模式结合使用。可以将请求封装成一个命令对象,然后在责任链中传递和处理这个命令对象。
⚝ 组合模式 (Composite Pattern): 如果处理者组织成树形结构(而非简单的链),组合模式可以用来构建这种结构。请求可以沿着树结构向下传递。
5.2 命令模式 (Command Pattern)
意图 (Intent)
将一个请求封装为一个对象,从而使你可用不同的请求、队列或日志来参数化客户,并支持可撤销 (undoable) 的操作。
动机 (Motivation)
假设您正在开发一个图形编辑器,其中有各种菜单项和按钮,例如“打开”、“保存”、“复制”、“粘贴”等。每当用户点击一个菜单项或按钮时,都会触发一个特定的操作。这些操作(请求)可能来源于不同的用户界面元素,但它们的核心行为是相似的:执行某个动作。
在传统的面向对象设计中,您可能会在用户界面元素的回调函数中直接调用执行操作的代码。但这会导致用户界面元素与具体操作之间产生紧密的耦合。如果需要支持宏录制、批处理、日志记录或操作的撤销/重做 (Undo/Redo) 功能,这种紧耦合的设计会变得非常复杂。
命令模式通过引入一个“命令”对象来解决这个问题。每个具体操作都被封装到一个独立的命令对象中,该对象包含了执行操作所需的所有信息(包括接收者和参数)。用户界面元素或其他客户端对象不再直接调用操作,而是创建一个命令对象,并通过一个通用的接口调用命令对象的execute
方法。这样,发送请求的对象(例如按钮)与执行请求的对象(例如文档对象)解耦,并且可以方便地对命令对象进行管理和操作。
结构 (Structure)
模式的关键参与者包括:
⚝ Command (命令):
▮▮▮▮⚝ 声明执行操作的接口,通常包含一个 execute()
方法。可能还包含 undo()
方法(如果支持撤销)。
⚝ ConcreteCommand (具体命令):
▮▮▮▮⚝ 实现 Command 接口。
▮▮▮▮⚝ 维护一个 Receiver (接收者) 对象的引用。
▮▮▮▮⚝ 将接收者的一个或多个动作绑定起来。在调用 execute()
时,它会调用接收者的相应操作。
⚝ Client (客户端):
▮▮▮▮⚝ 创建一个 ConcreteCommand 对象,并将 Receiver 对象设置给它。
⚝ Invoker (请求者):
▮▮▮▮⚝ 持有一个 Command 对象。
▮▮▮▮⚝ 在某个时候调用 Command 对象的 execute()
方法来发出请求。它不知道具体的命令类型或接收者。
⚝ Receiver (接收者):
▮▮▮▮⚝ 知道如何执行与请求相关的操作。任何类都可以充当接收者。
一个简化的UML类图结构(文本描述):
1
graph LR
2
Client --> ConcreteCommand
3
ConcreteCommand --|> Command
4
ConcreteCommand --> Receiver
5
Invoker --|> Command
6
Client --> Invoker
协作 (Collaboration)
① Client 创建一个 ConcreteCommand 实例,并配置它所需的 Receiver 对象和其他参数。
② Client 将这个 ConcreteCommand 对象设置给 Invoker。
③ 用户或某个事件触发 Invoker 的操作(例如点击按钮)。
④ Invoker 调用其持有的 Command 对象的 execute()
方法。
⑤ ConcreteCommand 的 execute()
方法调用其关联的 Receiver 对象上的具体操作来执行请求。
优点 (Advantages)
⚝ 发送者与接收者解耦 (Decoupling Sender from Receiver): 请求者无需知道具体操作是由哪个对象(Receiver)完成的,只需调用命令对象的 execute()
方法。这使得可以独立地改变请求者和接收者。
⚝ 易于扩展新的命令 (Ease of Adding New Commands): 添加新的操作只需创建新的 ConcreteCommand 类,无需修改现有类。
⚝ 支持可撤销/重做操作 (Support for Undo/Redo): 在 ConcreteCommand 中保存执行操作前的状态,并在 undo()
方法中恢复。Invoker 可以维护一个命令历史记录。
⚝ 支持宏命令和批处理 (Support for Macros and Batch Processing): 可以创建一个 CompositeCommand (组合命令),它包含多个 Command 对象,执行时依次调用它们的 execute()
方法。
⚝ 简化了请求的参数化 (Simplifies Parameterization of Requests): 可以用不同的命令对象来参数化请求者,或者将请求放入队列或日志中。
缺点 (Disadvantages)
⚝ 增加了类的数量 (Increased Number of Classes): 对于每一个具体操作,通常需要创建一个对应的 ConcreteCommand 类,可能导致类的数量膨胀。
⚝ 可能增加复杂性 (Increased Complexity): 特别是当需要支持撤销/重做时,管理命令对象的状态和历史记录会增加系统的复杂性。
C++实现示例 (C++ Implementation Example)
假设我们有一个简单的计算器程序,支持加法和减法操作,并且需要支持撤销。
1
#include <iostream>
2
#include <vector>
3
#include <memory> // For std::shared_ptr
4
5
// Receiver (接收者)
6
class Calculator {
7
public:
8
Calculator(int initialValue = 0) : m_value(initialValue) {}
9
10
void add(int operand) {
11
m_value += operand;
12
std::cout << "执行加法操作,当前值为: " << m_value << std::endl;
13
}
14
15
void subtract(int operand) {
16
m_value -= operand;
17
std::cout << "执行减法操作,当前值为: " << m_value << std::endl;
18
}
19
20
int getValue() const { return m_value; }
21
22
// 用于撤销操作,需要记录历史状态或者接收者能处理逆操作
23
// 在这个简单的例子中,接收者知道如何进行逆操作
24
void undoAdd(int operand) {
25
m_value -= operand;
26
std::cout << "撤销加法操作,当前值为: " << m_value << std::endl;
27
}
28
29
void undoSubtract(int operand) {
30
m_value += operand;
31
std::cout << "撤销减法操作,当前值为: " << m_value << std::endl;
32
}
33
34
private:
35
int m_value;
36
};
37
38
// Command (命令接口)
39
class Command {
40
public:
41
virtual ~Command() = default;
42
virtual void execute() = 0;
43
virtual void undo() = 0; // 支持撤销
44
};
45
46
// ConcreteCommand (具体命令):加法命令
47
class AddCommand : public Command {
48
public:
49
AddCommand(std::shared_ptr<Calculator> receiver, int operand) :
50
m_receiver(receiver), m_operand(operand) {}
51
52
void execute() override {
53
m_receiver->add(m_operand);
54
}
55
56
void undo() override {
57
m_receiver->undoAdd(m_operand);
58
}
59
60
private:
61
std::shared_ptr<Calculator> m_receiver;
62
int m_operand;
63
};
64
65
// ConcreteCommand (具体命令):减法命令
66
class SubtractCommand : public Command {
67
public:
68
SubtractCommand(std::shared_ptr<Calculator> receiver, int operand) :
69
m_receiver(receiver), m_operand(operand) {}
70
71
void execute() override {
72
m_receiver->subtract(m_operand);
73
}
74
75
void undo() override {
76
m_receiver->undoSubtract(m_operand);
77
}
78
79
private:
80
std::shared_ptr<Calculator> m_receiver;
81
int m_operand;
82
};
83
84
// Invoker (请求者)
85
class UserInterface {
86
public:
87
void setCommand(std::shared_ptr<Command> command) {
88
m_command = command;
89
}
90
91
void clickExecuteButton() {
92
if (m_command) {
93
m_command->execute();
94
m_history.push_back(m_command); // 记录已执行的命令
95
} else {
96
std::cout << "没有设置命令。" << std::endl;
97
}
98
}
99
100
void clickUndoButton() {
101
if (!m_history.empty()) {
102
std::shared_ptr<Command> lastCommand = m_history.back();
103
lastCommand->undo();
104
m_history.pop_back(); // 从历史记录中移除
105
} else {
106
std::cout << "没有可以撤销的命令。" << std::endl;
107
}
108
}
109
110
private:
111
std::shared_ptr<Command> m_command; // 当前设置的命令
112
std::vector<std::shared_ptr<Command>> m_history; // 命令历史记录,用于撤销
113
};
114
115
// Client (客户端)
116
int main() {
117
auto calculator = std::make_shared<Calculator>(10); // 初始值10
118
UserInterface ui;
119
120
// 用户点击加法按钮
121
auto addCmd = std::make_shared<AddCommand>(calculator, 5);
122
ui.setCommand(addCmd);
123
ui.clickExecuteButton(); // 执行加法: 10 + 5 = 15
124
125
// 用户点击减法按钮
126
auto subCmd = std::make_shared<SubtractCommand>(calculator, 3);
127
ui.setCommand(subCmd);
128
ui.clickExecuteButton(); // 执行减法: 15 - 3 = 12
129
130
// 用户点击撤销按钮
131
std::cout << "\n用户点击撤销..." << std::endl;
132
ui.clickUndoButton(); // 撤销减法: 12 + 3 = 15
133
134
// 用户再次点击撤销按钮
135
std::cout << "\n用户再次点击撤销..." << std::endl;
136
ui.clickUndoButton(); // 撤销加法: 15 - 5 = 10
137
138
// 用户再次点击撤销 (历史记录为空)
139
std::cout << "\n用户再次点击撤销..." << std::endl;
140
ui.clickUndoButton();
141
142
return 0;
143
}
在这个例子中,Calculator
是 Receiver,它知道如何执行实际的加法和减法操作。Command
是抽象命令接口,AddCommand
和 SubtractCommand
是具体的命令类,它们封装了对 Receiver 的调用。UserInterface
是 Invoker,它持有并执行命令,并负责管理命令的历史记录以支持撤销。Client (main函数) 负责创建这些对象并将它们组装起来。注意,为了支持撤销,命令对象需要保存执行操作所需的信息(例如加法/减法的操作数),并且 Receiver 需要提供相应的逆操作或者命令对象需要保存操作前的状态。
适用性 (Applicability)
命令模式适用于以下情况:
⚝ 需要将请求的发送者和接收者解耦。
⚝ 需要对命令进行排队、记录日志、或支持可撤销的操作。
⚝ 需要使用不同请求参数化对象,或者将请求封装成可操作的对象。
⚝ 需要支持宏命令 (Macro Command),即一个命令包含一系列子命令。
相关模式 (Related Patterns)
⚝ 备忘录模式 (Memento Pattern): 备忘录模式可以用于保存 Receiver 对象在执行命令前的状态,以便在撤销操作时恢复。
⚝ 组合模式 (Composite Pattern): 可以用组合模式来构建宏命令,即一个命令包含多个子命令。
⚝ 策略模式 (Strategy Pattern): 命令模式和策略模式在结构上有些相似(都有一个接口和多个实现类),但它们的意图不同。策略模式关注算法的可替换性,而命令模式关注将请求封装为对象。策略模式通常不包含 Receiver 的引用,而命令模式则包含。
5.3 迭代器模式 (Iterator Pattern)
意图 (Intent)
提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。## 5. 行为型模式:交互的智慧
欢迎来到本书的第五章,我们将深入探讨行为型设计模式 (Behavioral Patterns)。如果说创建型模式 (Creational Patterns) 关注对象的产生方式,结构型模式 (Structural Patterns) 关注类和对象的组合结构,那么行为型模式则聚焦于对象之间的算法和职责分配,以及它们如何相互协作以完成共同的任务。
行为型模式描述了对象或类如何相互协作以完成单个对象无法完成的任务。它们涉及算法、职责、通信等方面,提供了更加灵活和可维护的对象交互方式。理解和掌握行为型模式,能够帮助我们设计出职责清晰、耦合松散、易于扩展的软件系统。
本章将详细解析GoF (Gang of Four) 书中定义的全部行为型模式,包括它们的意图、动机、结构、参与者、协作方式、优缺点以及在C++语言中的具体实现和应用。我们还将结合现代C++的特性,探讨如何以更现代、更高效的方式来实现这些模式。希望通过本章的学习,您能更好地理解对象间的动态行为,提升设计复杂系统的能力。
5.1 责任链模式 (Chain of Responsibility Pattern)
意图 (Intent)
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
动机 (Motivation)
假设您正在设计一个事件处理系统,例如GUI应用程序中的鼠标点击或键盘输入,或者是一个审批流程系统。请求(事件)可能由多种不同类型的处理器来处理,但发送者不知道具体哪个处理器能处理这个请求,也不知道哪些处理器可能对这个请求感兴趣。如果发送者直接与所有可能的接收者耦合,那么当接收者集合发生变化时,发送者代码就需要修改,这违反了开放/封闭原则 (Open/Closed Principle)。
责任链模式提供了一种解决方案:将处理请求的对象组织成一条链。当一个请求到达时,它会沿着这条链依次传递。链上的每个对象都有机会处理请求。如果它能处理,就处理并可能终止传递;如果不能或不想处理,就将请求传递给链中的下一个对象。这样,请求的发送者只需要知道链的起点,而不需要知道链上的具体处理器,从而实现了发送者和接收者之间的解耦。
结构 (Structure)
模式的关键参与者包括:
⚝ Handler (处理者):
▮▮▮▮⚝ 定义一个处理请求的接口。
▮▮▮▮⚝ 实现后继者 (successor) 的链接。
▮▮▮▮⚝ 提供处理请求的方法,以及设置/获取后继者的方法。
⚝ ConcreteHandler (具体处理者):
▮▮▮▮⚝ 实现Handler接口。
▮▮▮▮⚝ 处理它负责的请求。如果不能处理,就将请求传递给它的后继者。
⚝ Client (客户端):
▮▮▮▮⚝ 创建处理者对象并组织成链。
▮▮▮▮⚝ 向链的头部发送请求。
一个简化的UML类图结构(文本描述):
1
graph LR
2
Client --> Handler
3
Handler <|-- ConcreteHandlerA
4
Handler <|-- ConcreteHandlerB
5
Handler --|> Handler: successor
协作 (Collaboration)
① 当一个请求到达链的头部(第一个ConcreteHandler)时,该处理者首先判断自己是否能处理这个请求。
② 如果能处理,它会执行相应的处理逻辑。这可能意味着请求处理结束,也可能意味着处理后还需要继续传递(取决于具体实现)。
③ 如果不能处理(或者需要继续传递),它会将请求转发给其后继者 (successor)。
④ 这个过程会沿着链条重复,直到请求被某个ConcreteHandler处理,或者到达链的末端仍未被处理。
优点 (Advantages)
⚝ 降低耦合度 (Decoupling): 发送者和接收者之间完全解耦。发送者只需向链的头部发送请求,无需知道具体的处理者。
⚝ 增强了给对象指派职责的灵活性 (Increased Flexibility): 可以在运行时动态地增加、删除或重排链中的处理者,从而改变处理请求的方式。
⚝ 简化了对象之间的连接 (Simplifies Connections): 每个处理者只需要知道其后继者,而不需要知道链上的所有其他处理者。
缺点 (Disadvantages)
⚝ 请求可能未被处理 (Request May Not Be Handled): 如果链中没有处理者能够处理特定的请求,请求可能会一直传递到链的末端而未被处理。需要额外的逻辑来处理这种情况(例如,在链的末端添加一个默认处理者)。
⚝ 性能问题 (Performance Issues): 请求必须沿着链传递,直到找到合适的处理者。如果链很长,或者请求处理者位于链的末端,可能会导致性能开销。
⚝ 可能导致循环 (Potential for Loops): 在构建链时,如果设置后继者不当,可能会导致请求在链中循环传递。
C++实现示例 (C++ Implementation Example)
假设我们有一个请假审批系统,审批流程为:组长 -> 部门经理 -> 总经理。
1
#include <iostream>
2
#include <string>
3
#include <memory> // For std::shared_ptr
4
5
// 定义请求类
6
class LeaveRequest {
7
public:
8
LeaveRequest(const std::string& name, int days) :
9
m_employeeName(name), m_leaveDays(days) {}
10
11
const std::string& getEmployeeName() const { return m_employeeName; }
12
int getLeaveDays() const { return m_leaveDays; }
13
14
private:
15
std::string m_employeeName;
16
int m_leaveDays;
17
};
18
19
// 抽象处理者 (Handler)
20
class Approver {
21
public:
22
Approver(const std::string& name) : m_name(name) {}
23
24
// 设置后继者
25
void setSuccessor(std::shared_ptr<Approver> successor) {
26
m_successor = successor;
27
}
28
29
// 处理请求的抽象方法
30
virtual void processRequest(const LeaveRequest& request) = 0;
31
32
protected:
33
std::string m_name; // 审批者姓名
34
std::shared_ptr<Approver> m_successor; // 后继审批者
35
};
36
37
// 具体处理者:组长 (ConcreteHandler)
38
class TeamLeader : public Approver {
39
public:
40
TeamLeader(const std::string& name) : Approver(name) {}
41
42
void processRequest(const LeaveRequest& request) override {
43
if (request.getLeaveDays() <= 1) {
44
std::cout << m_name << " (组长) 批准了 "
45
<< request.getEmployeeName() << " 的 "
46
<< request.getLeaveDays() << " 天请假申请。" << std::endl;
47
} else if (m_successor) {
48
std::cout << m_name << " (组长) 无权批准超过1天的请假,将请求转发给上级..." << std::endl;
49
m_successor->processRequest(request);
50
} else {
51
std::cout << m_name << " (组长) 无权批准超过1天的请假,且没有上级处理。" << std::endl;
52
}
53
}
54
};
55
56
// 具体处理者:部门经理 (ConcreteHandler)
57
class DepartmentManager : public Approver {
58
public:
59
DepartmentManager(const std::string& name) : Approver(name) {}
60
61
void processRequest(const LeaveRequest& request) override {
62
if (request.getLeaveDays() <= 3) {
63
std::cout << m_name << " (部门经理) 批准了 "
64
<< request.getEmployeeName() << " 的 "
65
<< request.getLeaveDays() << " 天请假申请。" << std::endl;
66
} else if (m_successor) {
67
std::cout << m_name << " (部门经理) 无权批准超过3天的请假,将请求转发给上级..." << std::endl;
68
m_successor->processRequest(request);
69
} else {
70
std::cout << m_name << " (部门经理) 无权批准超过3天的请假,且没有上级处理。" << std::endl;
71
}
72
}
73
};
74
75
// 具体处理者:总经理 (ConcreteHandler)
76
class GeneralManager : public Approver {
77
public:
78
GeneralManager(const std::string& name) : Approver(name) {}
79
80
void processRequest(const LeaveRequest& request) override {
81
if (request.getLeaveDays() <= 7) { // 假设总经理最多批准7天
82
std::cout << m_name << " (总经理) 批准了 "
83
<< request.getEmployeeName() << " 的 "
84
<< request.getLeaveDays() << " 天请假申请。" << std::endl;
85
} else if (m_successor) { // 总经理通常是链的最后一环,但保留扩展性
86
std::cout << m_name << " (总经理) 无权批准超过7天的请假,将请求转发给上级..." << std::endl;
87
m_successor->processRequest(request);
88
}
89
else {
90
std::cout << m_name << " (总经理) 无权批准超过7天的请假,审批流程结束,申请被拒绝。" << std::endl;
91
}
92
}
93
};
94
95
// 客户端代码 (Client)
96
int main() {
97
// 构建责任链
98
auto leader = std::make_shared<TeamLeader>("张三");
99
auto manager = std::make_shared<DepartmentManager>("李四");
100
auto general = std::make_shared<GeneralManager>("王五");
101
102
leader->setSuccessor(manager);
103
manager->setSuccessor(general);
104
// general->setSuccessor(nullptr); // 总经理是链的末端,不需要后继者或显式设为nullptr
105
106
// 发送请求
107
LeaveRequest req1("小明", 1);
108
std::cout << "发送请假申请: " << req1.getEmployeeName() << " 请假 " << req1.getLeaveDays() << " 天" << std::endl;
109
leader->processRequest(req1);
110
std::cout << "--------------------" << std::endl;
111
112
LeaveRequest req2("小红", 3);
113
std::cout << "发送请假申请: " << req2.getEmployeeName() << " 请假 " << req2.getLeaveDays() << " 天" << std::endl;
114
leader->processRequest(req2);
115
std::cout << "--------------------" << std::endl;
116
117
LeaveRequest req3("小刚", 5);
118
std::cout << "发送请假申请: " << req3.getEmployeeName() << " 请假 " << req3.getLeaveDays() << " 天" << std::endl;
119
leader->processRequest(req3);
120
std::cout << "--------------------" << std::endl;
121
122
LeaveRequest req4("小强", 10);
123
std::cout << "发送请假申请: " << req4.getEmployeeName() << " 请假 " << req4.getLeaveDays() << " 天" << std::endl;
124
leader->processRequest(req4);
125
std::cout << "--------------------" << std::endl;
126
127
return 0;
128
}
在C++实现中,我们通常使用抽象基类来定义Handler接口,并使用虚函数来实现多态。后继者的链接可以使用指针或智能指针 (Smart Pointers),这里使用了std::shared_ptr
来管理对象的生命周期。每个具体处理者在其processRequest
方法中实现自己的处理逻辑,并根据需要将请求传递给后继者。客户端负责构建链并启动请求。
适用性 (Applicability)
责任链模式适用于以下情况:
⚝ 有多个对象可以处理一个请求,但具体由哪个对象处理在运行时才能确定。
⚝ 想在不明确指定接收者的情况下,向多个对象中的一个提交请求。
⚝ 希望动态指定处理请求的对象集合,或者希望改变它们之间的处理顺序。
相关模式 (Related Patterns)
⚝ 命令模式 (Command Pattern): 责任链模式可以与命令模式结合使用。可以将请求封装成一个命令对象,然后在责任链中传递和处理这个命令对象。
⚝ 组合模式 (Composite Pattern): 如果处理者组织成树形结构(而非简单的链),组合模式可以用来构建这种结构。请求可以沿着树结构向下传递。
5.2 命令模式 (Command Pattern)
意图 (Intent)
将一个请求封装为一个对象,从而使你可用不同的请求、队列或日志来参数化客户,并支持可撤销 (undoable) 的操作。
动机 (Motivation)
假设您正在开发一个图形编辑器,其中有各种菜单项和按钮,例如“打开”、“保存”、“复制”、“粘贴”等。每当用户点击一个菜单项或按钮时,都会触发一个特定的操作。这些操作(请求)可能来源于不同的用户界面元素,但它们的核心行为是相似的:执行某个动作。
在传统的面向对象设计中,您可能会在用户界面元素的回调函数中直接调用执行操作的代码。但这会导致用户界面元素与具体操作之间产生紧密的耦合。如果需要支持宏录制、批处理、日志记录或操作的撤销/重做 (Undo/Redo) 功能,这种紧耦合的设计会变得非常复杂。
命令模式通过引入一个“命令”对象来解决这个问题。每个具体操作都被封装到一个独立的命令对象中,该对象包含了执行操作所需的所有信息(包括接收者和参数)。用户界面元素或其他客户端对象不再直接调用操作,而是创建一个命令对象,并通过一个通用的接口调用命令对象的execute
方法。这样,发送请求的对象(例如按钮)与执行请求的对象(例如文档对象)解耦,并且可以方便地对命令对象进行管理和操作。
结构 (Structure)
模式的关键参与者包括:
⚝ Command (命令):
▮▮▮▮⚝ 声明执行操作的接口,通常包含一个 execute()
方法。可能还包含 undo()
方法(如果支持撤销)。
⚝ ConcreteCommand (具体命令):
▮▮▮▮⚝ 实现 Command 接口。
▮▮▮▮⚝ 维护一个 Receiver (接收者) 对象的引用。
▮▮▮▮⚝ 将接收者的一个或多个动作绑定起来。在调用 execute()
时,它会调用接收者的相应操作。
⚝ Client (客户端):
▮▮▮▮⚝ 创建一个 ConcreteCommand 对象,并将 Receiver 对象设置给它。
⚝ Invoker (请求者):
▮▮▮▮⚝ 持有一个 Command 对象。
▮▮▮▮⚝ 在某个时候调用 Command 对象的 execute()
方法来发出请求。它不知道具体的命令类型或接收者。
⚝ Receiver (接收者):
▮▮▮▮⚝ 知道如何执行与请求相关的操作。任何类都可以充当接收者。
一个简化的UML类图结构(文本描述):
1
graph LR
2
Client --> ConcreteCommand
3
ConcreteCommand --|> Command
4
ConcreteCommand --> Receiver
5
Invoker --|> Command
6
Client --> Invoker
协作 (Collaboration)
① Client 创建一个 ConcreteCommand 实例,并配置它所需的 Receiver 对象和其他参数。
② Client 将这个 ConcreteCommand 对象设置给 Invoker。
③ 用户或某个事件触发 Invoker 的操作(例如点击按钮)。
④ Invoker 调用其持有的 Command 对象的 execute()
方法。
⑤ ConcreteCommand 的 execute()
方法调用其关联的 Receiver 对象上的具体操作来执行请求。
优点 (Advantages)
⚝ 发送者与接收者解耦 (Decoupling Sender from Receiver): 请求者无需知道具体操作是由哪个对象(Receiver)完成的,只需调用命令对象的 execute()
方法。这使得可以独立地改变请求者和接收者。
⚝ 易于扩展新的命令 (Ease of Adding New Commands): 添加新的操作只需创建新的 ConcreteCommand 类,无需修改现有类。
⚝ 支持可撤销/重做操作 (Support for Undo/Redo): 在 ConcreteCommand 中保存执行操作前的状态,并在 undo()
方法中恢复。Invoker 可以维护一个命令历史记录。
⚝ 支持宏命令和批处理 (Support for Macros and Batch Processing): 可以创建一个 CompositeCommand (组合命令),它包含多个 Command 对象,执行时依次调用它们的 execute()
方法。
⚝ 简化了请求的参数化 (Simplifies Parameterization of Requests): 可以用不同的命令对象来参数化请求者,或者将请求放入队列或日志中。
缺点 (Disadvantages)
⚝ 增加了类的数量 (Increased Number of Classes): 对于每一个具体操作,通常需要创建一个对应的 ConcreteCommand 类,可能导致类的数量膨胀。
⚝ 可能增加复杂性 (Increased Complexity): 特别是当需要支持撤销/重做时,管理命令对象的状态和历史记录会增加系统的复杂性。
C++实现示例 (C++ Implementation Example)
假设我们有一个简单的计算器程序,支持加法和减法操作,并且需要支持撤销。
1
#include <iostream>
2
#include <vector>
3
#include <memory> // For std::shared_ptr
4
5
// Receiver (接收者)
6
class Calculator {
7
public:
8
Calculator(int initialValue = 0) : m_value(initialValue) {}
9
10
void add(int operand) {
11
m_value += operand;
12
std::cout << "执行加法操作,当前值为: " << m_value << std::endl;
13
}
14
15
void subtract(int operand) {
16
m_value -= operand;
17
std::cout << "执行减法操作,当前值为: " << m_value << std::endl;
18
}
19
20
int getValue() const { return m_value; }
21
22
// 用于撤销操作,需要记录历史状态或者接收者能处理逆操作
23
// 在这个简单的例子中,接收者知道如何进行逆操作
24
void undoAdd(int operand) {
25
m_value -= operand;
26
std::cout << "撤销加法操作,当前值为: " << m_value << std::endl;
27
}
28
29
void undoSubtract(int operand) {
30
m_value += operand;
31
std::cout << "撤销减法操作,当前值为: " << m_value << std::endl;
32
}
33
34
private:
35
int m_value;
36
};
37
38
// Command (命令接口)
39
class Command {
40
public:
41
virtual ~Command() = default;
42
virtual void execute() = 0;
43
virtual void undo() = 0; // 支持撤销
44
};
45
46
// ConcreteCommand (具体命令):加法命令
47
class AddCommand : public Command {
48
public:
49
AddCommand(std::shared_ptr<Calculator> receiver, int operand) :
50
m_receiver(receiver), m_operand(operand) {}
51
52
void execute() override {
53
m_receiver->add(m_operand);
54
}
55
56
void undo() override {
57
m_receiver->undoAdd(m_operand);
58
}
59
60
private:
61
std::shared_ptr<Calculator> m_receiver;
62
int m_operand;
63
};
64
65
// ConcreteCommand (具体命令):减法命令
66
class SubtractCommand : public Command {
67
public:
68
SubtractCommand(std::shared_ptr<Calculator> receiver, int operand) :
69
m_receiver(receiver), m_operand(operand) {}
70
71
void execute() override {
72
m_receiver->subtract(m_operand);
73
}
74
75
void undo() override {
76
m_receiver->undoSubtract(m_operand);
77
}
78
79
private:
80
std::shared_ptr<Calculator> m_receiver;
81
int m_operand;
82
};
83
84
// Invoker (请求者)
85
class UserInterface {
86
public:
87
void setCommand(std::shared_ptr<Command> command) {
88
m_command = command;
89
}
90
91
void clickExecuteButton() {
92
if (m_command) {
93
m_command->execute();
94
m_history.push_back(m_command); // 记录已执行的命令
95
} else {
96
std::cout << "没有设置命令。" << std::endl;
97
}
98
}
99
100
void clickUndoButton() {
101
if (!m_history.empty()) {
102
std::shared_ptr<Command> lastCommand = m_history.back();
103
lastCommand->undo();
104
m_history.pop_back(); // 从历史记录中移除
105
} else {
106
std::cout << "没有可以撤销的命令。" << std::endl;
107
}
108
}
109
110
private:
111
std::shared_ptr<Command> m_command; // 当前设置的命令
112
std::vector<std::shared_ptr<Command>> m_history; // 命令历史记录,用于撤销
113
};
114
115
// Client (客户端)
116
int main() {
117
auto calculator = std::make_shared<Calculator>(10); // 初始值10
118
UserInterface ui;
119
120
// 用户点击加法按钮
121
auto addCmd = std::make_shared<AddCommand>(calculator, 5);
122
ui.setCommand(addCmd);
123
ui.clickExecuteButton(); // 执行加法: 10 + 5 = 15
124
125
// 用户点击减法按钮
126
auto subCmd = std::make_shared<SubtractCommand>(calculator, 3);
127
ui.setCommand(subCmd);
128
ui.clickExecuteButton(); // 执行减法: 15 - 3 = 12
129
130
// 用户点击撤销按钮
131
std::cout << "\n用户点击撤销..." << std::endl;
132
ui.clickUndoButton(); // 撤销减法: 12 + 3 = 15
133
134
// 用户再次点击撤销按钮
135
std::cout << "\n用户再次点击撤销..." << std::endl;
136
ui.clickUndoButton(); // 撤销加法: 15 - 5 = 10
137
138
// 用户再次点击撤销 (历史记录为空)
139
std::cout << "\n用户再次点击撤销..." << std::endl;
140
ui.clickUndoButton();
141
142
return 0;
143
}
在这个例子中,Calculator
是 Receiver,它知道如何执行实际的加法和减法操作。Command
是抽象命令接口,AddCommand
和 SubtractCommand
是具体的命令类,它们封装了对 Receiver 的调用。UserInterface
是 Invoker,它持有并执行命令,并负责管理命令的历史记录以支持撤销。Client (main函数) 负责创建这些对象并将它们组装起来。注意,为了支持撤销,命令对象需要保存执行操作所需的信息(例如加法/减法的操作数),并且 Receiver 需要提供相应的逆操作或者命令对象需要保存操作前的状态。
适用性 (Applicability)
命令模式适用于以下情况:
⚝ 需要将请求的发送者和接收者解耦。
⚝ 需要对命令进行排队、记录日志、或支持可撤销的操作。
⚝ 需要使用不同请求参数化对象,或者将请求封装成可操作的对象。
⚝ 需要支持宏命令 (Macro Command),即一个命令包含一系列子命令。
相关模式 (Related Patterns)
⚝ 备忘录模式 (Memento Pattern): 备忘录模式可以用于保存 Receiver 对象在执行命令前的状态,以便在撤销操作时恢复。
⚝ 组合模式 (Composite Pattern): 可以用组合模式来构建宏命令,即一个命令包含多个子命令。
⚝ 策略模式 (Strategy Pattern): 命令模式和策略模式在结构上有些相似(都有一个接口和多个实现类),但它们的意图不同。策略模式关注算法的可替换性,而命令模式关注将请求封装为对象。策略模式通常不包含 Receiver 的引用,而命令模式则包含。
5.3 迭代器模式 (Iterator Pattern)
意图 (Intent)
提供一种方法顺序访问一个聚合对象 (Aggregate Object) 中各个元素,而又不暴露该对象的内部表示。
动机 (Motivation)
考虑您正在使用一个复杂的数据结构,比如一个自定义的列表、树或图。您需要遍历这个数据结构中的所有元素来执行某些操作(例如打印、查找、计算总和)。不同的数据结构有不同的内部表示和遍历逻辑。如果直接在客户端代码中实现遍历逻辑,客户端代码将与具体的 数据结构紧密耦合。每当数据结构的内部表示改变,或者需要支持新的遍历方式时(例如前序遍历、后序遍历),客户端代码都需要修改。
迭代器模式的目的是将遍历逻辑从聚合对象中分离出来。它提供一个统一的接口,使得客户端可以以一种标准的方式访问聚合对象的元素,而无需了解其底层实现细节。不同的数据结构可以提供不同的迭代器,但这些迭代器都遵循相同的迭代器接口。
结构 (Structure)
模式的关键参与者包括:
⚝ Iterator (迭代器):
▮▮▮▮⚝ 定义访问和遍历元素的接口。通常包括:获取第一个元素 (first()
)、获取下一个元素 (next()
)、判断是否遍历结束 (isDone()
)、获取当前元素 (currentItem()
) 等方法。
⚝ ConcreteIterator (具体迭代器):
▮▮▮▮⚝ 实现 Iterator 接口。
▮▮▮▮⚝ 维护遍历聚合对象所需的状态(例如当前位置)。
⚝ Aggregate (聚合对象):
▮▮▮▮⚝ 定义创建相应迭代器对象的接口,通常是 createIterator()
方法。
⚝ ConcreteAggregate (具体聚合对象):
▮▮▮▮⚝ 实现 Aggregate 接口。
▮▮▮▮⚝ 返回一个 ConcreteIterator 实例,该实例负责遍历自己。
一个简化的UML类图结构(文本描述):
1
graph LR
2
Client --> Aggregate
3
Client --> Iterator
4
Aggregate --> ConcreteAggregate
5
Aggregate ..> Iterator: createIterator() returns
6
Iterator --> ConcreteIterator
7
ConcreteIterator --> ConcreteAggregate
协作 (Collaboration)
① Client 通过 ConcreteAggregate 的 createIterator()
方法获取一个 ConcreteIterator 实例。
② Client 使用 Iterator 接口提供的 first()
、next()
、isDone()
和 currentItem()
等方法来顺序访问聚合对象中的元素。
③ ConcreteIterator 在遍历过程中与 ConcreteAggregate 交互,访问其内部数据,并维护当前位置。
优点 (Advantages)
⚝ 分离了遍历逻辑和聚合对象 (Separates Traversal Logic from Aggregate): 使聚合对象更专注于管理其内部数据,而迭代器则专注于遍历。
⚝ 支持多种遍历方式 (Supports Multiple Traversal Methods): 可以为同一个聚合对象提供多个不同的迭代器,每个迭代器实现一种特定的遍历算法。
⚝ 为访问不同的聚合结构提供了统一接口 (Provides a Unified Interface for Accessing Different Aggregates): 客户端可以使用相同的迭代器接口遍历不同类型的聚合对象。
⚝ 简化了聚合对象的接口 (Simplifies the Aggregate Interface): 聚合对象不再需要提供大量的遍历方法。
缺点 (Disadvantages)
⚝ 对于简单的遍历可能显得过度设计 (Overhead for Simple Traversals): 对于简单的聚合对象,引入迭代器模式可能会增加额外的类和复杂性。
⚝ 迭代器的生命周期管理 (Iterator Lifecycle Management): 在某些实现中,需要注意迭代器和聚合对象之间的生命周期依赖关系,避免悬空指针等问题。
C++实现示例 (C++ Implementation Example)
我们创建一个简单的自定义链表 (List) 作为聚合对象,并为其实现一个迭代器。
1
#include <iostream>
2
#include <vector> // For std::vector example
3
4
// 前向声明 Aggregate
5
template <typename T>
6
class CustomList;
7
8
// 抽象迭代器接口 (Iterator)
9
template <typename T>
10
class Iterator {
11
public:
12
virtual ~Iterator() = default;
13
virtual void first() = 0; // 指向第一个元素
14
virtual void next() = 0; // 指向下一个元素
15
virtual bool isDone() const = 0; // 判断是否遍历结束
16
virtual T& currentItem() = 0; // 获取当前元素
17
};
18
19
// 具体聚合对象:自定义链表 (ConcreteAggregate)
20
template <typename T>
21
class CustomList {
22
private:
23
struct Node {
24
T data;
25
Node* next = nullptr;
26
};
27
Node* head = nullptr;
28
Node* tail = nullptr;
29
int size = 0;
30
31
public:
32
CustomList() = default;
33
~CustomList() {
34
Node* current = head;
35
while (current != nullptr) {
36
Node* next = current->next;
37
delete current;
38
current = next;
39
}
40
}
41
42
void append(const T& value) {
43
Node* newNode = new Node{value, nullptr};
44
if (!head) {
45
head = newNode;
46
tail = newNode;
47
} else {
48
tail->next = newNode;
49
tail = newNode;
50
}
51
size++;
52
}
53
54
int getSize() const { return size; }
55
Node* getHead() const { return head; } // 暴露内部结构给迭代器
56
57
// 创建迭代器 (createIterator)
58
std::unique_ptr<Iterator<T>> createIterator();
59
60
private:
61
// 阻止拷贝和赋值,简化示例
62
CustomList(const CustomList&) = delete;
63
CustomList& operator=(const CustomList&) = delete;
64
};
65
66
// 具体迭代器 (ConcreteIterator)
67
template <typename T>
68
class CustomListIterator : public Iterator<T> {
69
private:
70
CustomList<T>* m_list; // 持有聚合对象引用
71
typename CustomList<T>::Node* m_current; // 遍历当前位置
72
73
public:
74
CustomListIterator(CustomList<T>* list) : m_list(list), m_current(nullptr) {
75
if (m_list) {
76
first(); // 创建时立即指向第一个元素
77
}
78
}
79
80
void first() override {
81
if (m_list) {
82
m_current = m_list->getHead();
83
} else {
84
m_current = nullptr;
85
}
86
}
87
88
void next() override {
89
if (m_current) {
90
m_current = m_current->next;
91
}
92
}
93
94
bool isDone() const override {
95
return m_current == nullptr;
96
}
97
98
T& currentItem() override {
99
if (isDone()) {
100
throw std::out_of_range("Iterator out of bounds");
101
}
102
return m_current->data;
103
}
104
};
105
106
// 实现 CustomList 的 createIterator 方法
107
template <typename T>
108
std::unique_ptr<Iterator<T>> CustomList<T>::createIterator() {
109
// 这里使用 unique_ptr 确保迭代器的所有权清晰
110
return std::make_unique<CustomListIterator<T>>(this);
111
}
112
113
114
// 客户端代码 (Client)
115
int main() {
116
CustomList<int> my_list;
117
my_list.append(10);
118
my_list.append(20);
119
my_list.append(30);
120
121
// 使用迭代器遍历
122
std::unique_ptr<Iterator<int>> it = my_list.createIterator();
123
124
std::cout << "使用自定义迭代器遍历 CustomList:" << std::endl;
125
for (it->first(); !it->isDone(); it->next()) {
126
std::cout << "当前元素: " << it->currentItem() << std::endl;
127
}
128
std::cout << "--------------------" << std::endl;
129
130
// 对比 C++ 标准库中的迭代器
131
std::vector<int> my_vector = {100, 200, 300};
132
std::cout << "使用 C++ 标准库迭代器遍历 std::vector:" << std::endl;
133
for (auto vec_it = my_vector.begin(); vec_it != my_vector.end(); ++vec_it) {
134
std::cout << "当前元素: " << *vec_it << std::endl;
135
}
136
std::cout << "--------------------" << std::endl;
137
138
return 0;
139
}
在这个例子中,CustomList
是 ConcreteAggregate
,它维护链表节点并提供 createIterator
方法。Iterator
是抽象迭代器接口,CustomListIterator
是 ConcreteIterator
,它实现了遍历链表的逻辑,并持有一个指向 CustomList
的指针以及当前遍历到的节点指针。客户端通过聚合对象获取迭代器,然后使用统一的迭代器接口进行遍历。
适用性 (Applicability)
迭代器模式适用于以下情况:
⚝ 需要访问一个聚合对象的内容,而又不暴露其内部表示。
⚝ 需要支持对同一个聚合对象进行多种不同的遍历。
⚝ 需要提供一个统一的接口来遍历不同类型的聚合对象。
相关模式 (Related Patterns)
⚝ 组合模式 (Composite Pattern): 迭代器常用于遍历组合模式构建的树形结构。
⚝ 工厂方法模式 (Factory Method Pattern): 聚合对象可以使用工厂方法来创建其对应的迭代器对象,使得迭代器类的创建延迟到子类,允许聚合对象的子类返回不同类型的迭代器。
5.3.1 C++标准库中的迭代器 (Iterators in C++ Standard Library)
C++标准库 (C++ Standard Library) 广泛使用了迭代器模式,这是STL (Standard Template Library) 的核心设计思想之一。STL 的容器 (Containers) (如 std::vector
, std::list
, std::map
, std::set
等) 充当聚合对象,它们都提供 begin()
和 end()
方法来返回迭代器,这些迭代器充当具体迭代器。STL 的算法 (Algorithms) (如 std::sort
, std::find
, std::for_each
等) 则充当客户端,它们通过迭代器接口操作容器中的元素,而无需关心容器的具体类型或内部实现。
C++标准库中的迭代器并非使用传统的基于虚函数的面向对象多态,而是使用了模板和概念 (Concepts) (C++20引入,但其思想早已有之) 实现的编译时多态。迭代器的行为由其支持的操作集定义,而不是继承自一个公共基类。这种方式提供了更高的性能,并且更加灵活。
C++迭代器根据其能力被划分为不同的类别(C++20后称为概念),形成一个层级结构:
① Input Iterator (输入迭代器): 只能单向移动 (++it
),只能读取元素 (*it
作为右值),只能比较相等性 (==
, !=
)。可以进行多次一趟遍历 (multi-pass),但通常只用于单趟遍历 (single-pass)。例如:istream_iterator。
② Output Iterator (输出迭代器): 只能单向移动 (++it
),只能写入元素 (*it
作为左值)。通常只用于单趟遍历。例如:ostream_iterator。
③ Forward Iterator (前向迭代器): 兼具输入迭代器和输出迭代器的能力,可以多次一趟遍历。支持 ++it
。例如:forward_list 的迭代器。
④ Bidirectional Iterator (双向迭代器): 在前向迭代器的基础上,增加了向后移动的能力 (--it
)。例如:list 和 set 的迭代器。
⑤ Random Access Iterator (随机访问迭代器): 在双向迭代器的基础上,增加了随机访问的能力 (it + n
, it - n
, it[n]
),可以像指针一样进行算术运算和直接访问。例如:vector, deque, array 的迭代器。
这种分类使得算法可以根据所需的最小迭代器能力来指定参数,从而提高算法的通用性。
C++迭代器示例:
1
#include <iostream>
2
#include <vector>
3
#include <list>
4
#include <algorithm> // For std::for_each, std::sort
5
6
int main() {
7
// Random Access Iterator (vector)
8
std::vector<int> vec = {1, 5, 2, 8, 3};
9
std::cout << "Vector elements:" << std::endl;
10
// 使用范围for循环,底层依赖迭代器
11
for (int x : vec) {
12
std::cout << x << " ";
13
}
14
std::cout << std::endl;
15
16
// 使用迭代器和算法
17
std::sort(vec.begin(), vec.end()); // sort 需要 Random Access Iterator
18
19
std::cout << "Sorted vector elements:" << std::endl;
20
for (auto it = vec.begin(); it != vec.end(); ++it) {
21
std::cout << *it << " ";
22
}
23
std::cout << std::endl;
24
std::cout << "--------------------" << std::endl;
25
26
// Bidirectional Iterator (list)
27
std::list<int> lst = {10, 50, 20, 40, 30};
28
std::cout << "List elements:" << std::endl;
29
for (int x : lst) {
30
std::cout << x << " ";
31
}
32
std::cout << std::endl;
33
34
// list 的迭代器不支持随机访问,所以不能直接使用 std::sort
35
// 但支持双向移动
36
auto it_end = lst.end();
37
--it_end; // 向后移动
38
std::cout << "Last element of list: " << *it_end << std::endl;
39
40
// 可以使用适用于双向迭代器的算法,例如 std::reverse
41
std::reverse(lst.begin(), lst.end());
42
std::cout << "Reversed list elements:" << std::endl;
43
for (int x : lst) {
44
std::cout << x << " ";
45
}
46
std::cout << std::endl;
47
std::cout << "--------------------" << std::endl;
48
49
return 0;
50
}
分析C++标准库迭代器的设计:
① 轻量级 (Lightweight): STL 迭代器通常是很小的对象,通常只包含指向容器内部元素的指针或索引。
② 基于模板 (Template-Based): 通过模板参数 T 指定元素的类型,使得迭代器可以用于任何类型的元素。
③ 运算符重载 (Operator Overloading): 使用重载运算符(如 *
, ->
, ++
, --
, ==
, !=
, +
, -
, []
)来提供直观的语法,使其使用起来类似于指针。
④ Concepts (概念): C++20 引入了 Concepts 来更正式地描述迭代器类别所需的接口和语义,增强了编译时检查能力。例如,std::random_access_iterator
概念精确地定义了随机访问迭代器必须支持的操作。
⑤ Range-based for loop (基于范围的for循环): C++11 引入的范围for循环是迭代器模式的一种语法糖,极大地简化了遍历容器的代码,其底层就是通过调用 begin()
和 end()
获取迭代器来实现的。
C++标准库中的迭代器是迭代器模式在C++中非常成功的应用案例,展示了模板和编译时多态在实现通用、高效库组件方面的强大能力。
5.4 中介者模式 (Mediator Pattern)
意图 (Intent)
用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
动机 (Motivation)
假设您正在设计一个GUI对话框,其中包含各种控件,如按钮、文本框、下拉列表等。这些控件之间存在复杂的交互关系:点击一个按钮可能会影响多个文本框的状态,选择下拉列表中的项可能会启用或禁用其他控件。如果让这些控件直接相互引用和通信,那么每个控件都需要知道其他相关控件的存在,并且维护它们之间的复杂依赖关系。随着控件数量的增加,这种直接耦合会导致代码难以维护和扩展,形成“网状结构”的依赖关系。
中介者模式提供了一种解决方案:引入一个中介者对象 (Mediator),所有的控件(称为同事对象/Colleagues)都不再直接相互通信,而是都与中介者通信。当一个同事对象发生变化或产生事件时,它通知中介者。中介者根据具体业务逻辑,协调并通知其他同事对象做出相应的响应。这样,同事对象之间不再相互依赖,而是只依赖于中介者。中介者负责集中控制这些复杂的交互逻辑。
结构 (Structure)
模式的关键参与者包括:
⚝ Mediator (中介者):
▮▮▮▮⚝ 定义一个接口,用于同事对象与中介者之间的通信。
⚝ ConcreteMediator (具体中介者):
▮▮▮▮⚝ 实现 Mediator 接口。
▮▮▮▮⚝ 维护对其管理的各个 Colleague 对象的引用。
▮▮▮▮⚝ 实现具体的协调行为,知道所有同事对象及其相互关系。当收到某个同事对象的消息时,会根据情况向其他同事对象发出指令。
⚝ Colleague (同事对象):
▮▮▮▮⚝ 定义与中介者通信的接口。
▮▮▮▮⚝ 维护对其 Mediator 对象的引用。
⚝ ConcreteColleague (具体同事对象):
▮▮▮▮⚝ 实现 Colleague 接口。
▮▮▮▮⚝ 每个具体同事对象只知道与中介者通信,而不知道其他具体同事对象。当发生重要事件时,它会通知中介者。
一个简化的UML类图结构(文本描述):
1
graph LR
2
Client --> ConcreteMediator
3
Client --> ConcreteColleagueA
4
Client --> ConcreteColleagueB
5
Mediator <|-- ConcreteMediator
6
Colleague <|-- ConcreteColleagueA
7
Colleague <|-- ConcreteColleagueB
8
ConcreteMediator --> ConcreteColleagueA
9
ConcreteMediator --> ConcreteColleagueB
10
ConcreteColleagueA --|> Mediator
11
ConcreteColleagueB --|> Mediator
注意:上图中的同事对象指向中介者是重要的通信方向。中介者则通过直接引用或通过接口调用同事对象的方法来协调它们。
协作 (Collaboration)
① Colleague 对象知道它们的中介者,并且只与中介者通信。
② 每个 Colleague 在自身状态改变时,通知其 Mediator。
③ Mediator 接收到通知后,根据预设的规则,与其他 Colleague 对象进行交互,触发它们的行为。
④ Colleague 对象接收到来自 Mediator 的指令,执行相应的操作,但它们不直接与发出原始通知的 Colleague 对象交互。
优点 (Advantages)
⚝ 减少了子类化 (Reduces Subclassing): 由于交互行为被集中到中介者中,同事类之间的耦合减少,不需要为处理特定的交互行为而创建同事类的子类。
⚝ 将对象间的耦合解耦 (Decouples Colleagues): 同事对象之间不再直接引用,降低了它们之间的耦合度。
⚝ 集中控制交互 (Centralizes Control): 将复杂的交互逻辑从散布在各个同事类中的代码集中到一个中介者类中,使得交互逻辑更易于理解、管理和修改。
⚝ 易于独立地改变交互行为 (Easier to Change Interactions Independently): 改变对象之间的交互方式只需修改中介者,而不需要修改各个同事类。
缺点 (Disadvantages)
⚝ 中介者可能变得过于复杂 (Mediator Can Become a Monolith): 如果系统中的交互逻辑非常复杂,中介者可能会承担过多的职责,变成一个“上帝对象” (God Object),难以维护。
⚝ 可能降低性能 (Potential Performance Issues): 所有通信都经过中介者,可能会增加一些间接性开销。
C++实现示例 (C++ Implementation Example)
模拟一个简单的聊天室,用户 (User) 是同事对象,聊天室 (ChatRoom) 是中介者。
1
#include <iostream>
2
#include <string>
3
#include <vector>
4
#include <memory> // For std::shared_ptr
5
#include <algorithm> // For std::remove
6
7
// 前向声明 Mediator
8
class ChatRoomMediator;
9
10
// Colleague (同事对象)
11
class User {
12
protected:
13
std::string m_name;
14
std::shared_ptr<ChatRoomMediator> m_mediator; // 持有中介者的引用
15
16
public:
17
User(const std::string& name) : m_name(name) {}
18
19
void setMediator(std::shared_ptr<ChatRoomMediator> mediator) {
20
m_mediator = mediator;
21
}
22
23
const std::string& getName() const { return m_name; }
24
25
// 由中介者调用,接收消息
26
virtual void receiveMessage(const std::string& message) const {
27
std::cout << "[" << m_name << "] 接收到消息: " << message << std::endl;
28
}
29
30
// 向中介者发送消息
31
virtual void sendMessage(const std::string& message) const; // 在 ChatRoomMediator 定义后实现
32
};
33
34
// Mediator (中介者接口)
35
class ChatRoomMediator {
36
public:
37
virtual ~ChatRoomMediator() = default;
38
39
// 同事对象通过此方法发送消息给其他同事
40
virtual void sendMessage(const std::string& message, const User* sender) = 0;
41
42
// 注册用户到聊天室
43
virtual void addUser(std::shared_ptr<User> user) = 0;
44
};
45
46
// ConcreteMediator (具体中介者):聊天室
47
class ChatRoom : public ChatRoomMediator, public std::enable_shared_from_this<ChatRoom> {
48
private:
49
std::vector<std::shared_ptr<User>> m_users;
50
51
public:
52
void sendMessage(const std::string& message, const User* sender) override {
53
// 广播消息给所有其他用户
54
for (const auto& user : m_users) {
55
if (user.get() != sender) { // 不发给发送者自己
56
user->receiveMessage("来自 [" + sender->getName() + "]: " + message);
57
}
58
}
59
}
60
61
void addUser(std::shared_ptr<User> user) override {
62
m_users.push_back(user);
63
// 设置用户的中介者引用为当前聊天室
64
user->setMediator(shared_from_this());
65
std::cout << "[聊天室] " << user->getName() << " 加入了聊天室。" << std::endl;
66
}
67
68
void removeUser(const User* userToRemove) {
69
m_users.erase(
70
std::remove_if(m_users.begin(), m_users.end(),
71
[&](const std::shared_ptr<User>& user) {
72
return user.get() == userToRemove;
73
}),
74
m_users.end()
75
);
76
std::cout << "[聊天室] " << userToRemove->getName() << " 离开了聊天室。" << std::endl;
77
}
78
};
79
80
// 实现 User::sendMessage,因为它需要 ChatRoomMediator 的定义
81
void User::sendMessage(const std::string& message) const {
82
if (m_mediator) {
83
m_mediator->sendMessage(message, this);
84
} else {
85
std::cout << "[" << m_name << "] 没有加入聊天室,无法发送消息。" << std::endl;
86
}
87
}
88
89
// 客户端代码 (Client)
90
int main() {
91
// 创建中介者
92
auto chatRoom = std::make_shared<ChatRoom>();
93
94
// 创建同事对象
95
auto user1 = std::make_shared<User>("Alice");
96
auto user2 = std::make_shared<User>("Bob");
97
auto user3 = std::make_shared<User>("Charlie");
98
99
// 将用户添加到聊天室 (中介者会自动设置中介者引用)
100
chatRoom->addUser(user1);
101
chatRoom->addUser(user2);
102
chatRoom->addUser(user3);
103
104
std::cout << "\n--- 开始聊天 ---" << std::endl;
105
106
// 用户通过中介者发送消息
107
user1->sendMessage("大家好!");
108
std::cout << std::endl;
109
110
user2->sendMessage("Alice 你好!");
111
std::cout << std::endl;
112
113
user3->sendMessage("Bob 也在呀!");
114
std::cout << std::endl;
115
116
std::cout << "--- 聊天结束 ---\n" << std::endl;
117
118
// 模拟用户离开
119
chatRoom->removeUser(user2.get());
120
std::cout << std::endl;
121
122
// Alice 再次发送消息,Bob 收不到
123
user1->sendMessage("Bob 怎么没回应?");
124
std::cout << std::endl;
125
126
127
return 0;
128
}
在这个聊天室的例子中,ChatRoom
是 ConcreteMediator
,它管理所有 User
(ConcreteColleague) 对象。每个 User
对象都持有一个指向 ChatRoom
的引用。当一个 User
需要发送消息时,它不是直接向其他 User
发送,而是调用 m_mediator->sendMessage()
方法。ChatRoom
接收到消息后,遍历所有注册的用户,并调用他们的 receiveMessage
方法,从而将消息广播出去(不发给发送者自己)。这样,User
对象之间相互解耦,它们只依赖于 ChatRoom
这个中介者来协调通信。std::enable_shared_from_this
用于让 ChatRoom
对象能够获取自身的 shared_ptr
,以便在 addUser
中设置给 User
对象。
适用性 (Applicability)
中介者模式适用于以下情况:
⚝ 对象之间存在复杂的、网状的交互关系,导致类之间的耦合度很高。
⚝ 当对象间的交互行为难以复用或维护时。
⚝ 当需要定制一个分布在多个类中的行为,而不想生成太多的子类时。
相关模式 (Related Patterns)
⚝ 观察者模式 (Observer Pattern): 观察者模式中,主题 (Subject) 和观察者 (Observer) 之间的交互也是一对多的。区别在于,观察者模式中主题直接通知观察者,观察者之间可能通过主题间接交互;而中介者模式中,同事对象之间不直接交互,所有交互都由中介者协调。中介者模式可以看作是观察者模式的一种特殊应用,其中中介者是多个同事对象的观察者和主题。
⚝ 外观模式 (Facade Pattern): 外观模式为子系统提供一个统一的高层接口,使得子系统更容易使用。中介者模式则用于协调子系统内部多个对象之间的交互。外观模式隐藏子系统的复杂性,中介者模式解决对象间复杂的交互问题。
⚝ 单例模式 (Singleton Pattern): 中介者对象在某些系统中可能只需要一个实例,此时可以考虑结合单例模式。
5.5 备忘录模式 (Memento Pattern)
意图 (Intent)
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到保存的状态。
动机 (Motivation)
假设您正在开发一个文本编辑器或者一个图形绘制程序。用户在编辑或绘制过程中可能会执行一系列操作,并且希望能够撤销 (Undo) 最近的操作。要实现撤销功能,您需要保存对象在执行操作前的状态,以便在需要撤销时恢复到这个状态。
最直接的方法可能是在需要保存状态时,让对象将其所有关键状态数据暴露出来,然后由另一个对象(例如撤销管理器)来存储这些数据。但是,这样做会破坏对象的封装性,将内部细节暴露给外部,使得对象的内部表示难以修改。
备忘录模式提供了一种方式,允许在不违反封装原则的情况下,捕获和外部化对象的内部状态。模式定义了一个“备忘录”对象 (Memento),用于存储原发器 (Originator) 对象的内部状态。原发器负责创建备忘录,并使用备忘录恢复自身状态。另一个对象,看管者 (Caretaker),负责保存备忘录,但它不能查看或修改备忘录的内容,只能将其传递给原发器。
结构 (Structure)
模式的关键参与者包括:
⚝ Originator (原发器):
▮▮▮▮⚝ 创建一个包含自身当前内部状态的 Memento 对象。
▮▮▮▮⚝ 使用 Memento 对象恢复自身状态。
⚝ Memento (备忘录):
▮▮▮▮⚝ 存储 Originator 对象的内部状态。备忘录可以对外提供一个窄接口 (Narrow Interface),供 Caretaker 使用(例如一个标识符),以及一个宽接口 (Wide Interface),供 Originator 使用(用于访问所有状态数据)。对 Caretaker 来说,备忘录是透明的或只提供有限访问,对其内部状态一无所知;对 Originator 来说,它可以访问备忘录中的所有数据。
⚝ Caretaker (看管者):
▮▮▮▮⚝ 负责保存 Memento 对象,但不能检查 Memento 的内容。
▮▮▮▮⚝ 在需要时,将 Memento 对象提供给 Originator,以便恢复状态。
一个简化的UML类图结构(文本描述):
1
graph LR
2
Originator --> Memento
3
Caretaker --> Memento
4
Originator --> Caretaker
5
Caretaker -- manage --> Memento
6
Originator -- create/restore --> Memento
7
Caretaker -- save/get --> Memento
协作 (Collaboration)
① 客户端 (Client) (通常是 Caretaker 或者与 Caretaker 协作的对象) 在需要保存 Originator 状态时,通知 Originator 创建一个备忘录 (createMemento()
)。
② Originator 创建一个 Memento 对象,将自己的当前状态存储到其中,并将其返回给客户端。
③ 客户端 (Caretaker) 接收到 Memento 对象后,将其保存起来,但不访问其内部状态。
④ 当需要撤销或恢复 Originator 的状态时,客户端从 Caretaker 获取之前保存的 Memento 对象。
⑤ 客户端将获取到的 Memento 对象传递给 Originator 的恢复方法 (restoreMemento(memento)
)。
⑥ Originator 使用 Memento 中的状态信息恢复自身到之前的状态。
优点 (Advantages)
⚝ 保护了封装性 (Preserves Encapsulation): Originator 的内部状态被封装在 Memento 对象中,外部对象(Caretaker)无法直接访问或修改这些状态,保证了 Originator 的封装性。
⚝ 简化了 Originator (Simplifies the Originator): Originator 不需要关心如何存储和管理历史状态,这部分职责由 Memento 和 Caretaker 分担。
⚝ 支持撤销操作 (Supports Undo Operations): 备忘录模式是实现撤销/重做功能的基础。
缺点 (Disadvantages)
⚝ 状态存储的开销 (Storage Overhead): 如果 Originator 的状态非常大,创建和存储大量 Memento 对象可能会消耗大量的内存资源。
⚝ Originator 需要暴露一些接口 (Originator Needs to Expose Interface): Originator 必须提供创建备忘录 (createMemento()
) 和从备忘录恢复 (restoreMemento()
) 的接口,这可能需要暴露一部分内部状态或方法给备忘录类(如果备忘录是 Originator 的内部类或者友元)。
⚝ 某些语言实现困难 (Implementation Difficulty in Some Languages): 在C++中,如果 Memento 需要访问 Originator 的私有状态,可以将其定义为 Originator 的内部类或友元类,但这增加了类之间的耦合。
C++实现示例 (C++ Implementation Example)
模拟一个简单的文本编辑器,支持输入文本和撤销。
1
#include <iostream>
2
#include <string>
3
#include <vector>
4
#include <memory> // For std::shared_ptr
5
6
// 前向声明 Originator
7
class EditorOriginator;
8
9
// Memento (备忘录)
10
// 通常备忘录会是 Originator 的内部类,以访问其私有状态
11
class EditorMemento {
12
private:
13
std::string m_content; // 存储 Originator 的状态
14
15
// 构造函数和状态访问方法通常只有 Originator 才能访问
16
EditorMemento(const std::string& content) : m_content(content) {}
17
18
const std::string& getContent() const { return m_content; }
19
20
// 将 Originator 设置为友元,使其可以访问备忘录的私有成员
21
friend class EditorOriginator;
22
// 将 Caretaker 设置为友元(如果 Caretaker 需要通过窄接口访问,这里不需要)
23
// friend class EditorCaretaker;
24
25
public:
26
// 备忘录也可以提供一个窄接口供外部(如 Caretaker)使用,例如获取时间戳或名称
27
// 但不能暴露内部状态
28
// std::string getTimestamp() const { return "..."; }
29
30
// 外部通常不能直接创建或修改备忘录
31
~EditorMemento() = default; // 允许销毁
32
};
33
34
// Originator (原发器)
35
class EditorOriginator : public std::enable_shared_from_this<EditorOriginator> {
36
private:
37
std::string m_currentContent; // 需要保存的状态
38
39
public:
40
EditorOriginator(const std::string& initialContent = "") : m_currentContent(initialContent) {}
41
42
void type(const std::string& text) {
43
m_currentContent += text;
44
std::cout << "输入文本: \"" << text << "\"" << std::endl;
45
std::cout << "当前内容: \"" << m_currentContent << "\"" << std::endl;
46
}
47
48
// 创建备忘录 (createMemento)
49
std::shared_ptr<EditorMemento> createMemento() const {
50
std::cout << "创建备忘录,保存当前状态..." << std::endl;
51
// Originator 可以访问 Memento 的私有构造函数
52
return std::shared_ptr<EditorMemento>(new EditorMemento(m_currentContent));
53
}
54
55
// 从备忘录恢复状态 (restoreMemento)
56
void restoreMemento(std::shared_ptr<EditorMemento> memento) {
57
if (memento) {
58
// Originator 可以访问 Memento 的私有状态访问方法
59
m_currentContent = memento->getContent();
60
std::cout << "从备忘录恢复状态..." << std::endl;
61
std::cout << "当前内容: \"" << m_currentContent << "\"" << std::endl;
62
} else {
63
std::cout << "备忘录无效,无法恢复。" << std::endl;
64
}
65
}
66
67
const std::string& getContent() const { return m_currentContent; }
68
};
69
70
// Caretaker (看管者)
71
class EditorCaretaker {
72
private:
73
std::vector<std::shared_ptr<EditorMemento>> m_history; // 保存备忘录的历史记录
74
75
public:
76
void save(std::shared_ptr<EditorMemento> memento) {
77
m_history.push_back(memento);
78
std::cout << "看管者保存了备忘录。历史记录大小: " << m_history.size() << std::endl;
79
}
80
81
std::shared_ptr<EditorMemento> undo() {
82
if (!m_history.empty()) {
83
std::shared_ptr<EditorMemento> memento = m_history.back();
84
m_history.pop_back(); // 移除当前的最新状态
85
std::cout << "看管者取出最后一个备忘录进行撤销。剩余历史记录大小: " << m_history.size() << std::endl;
86
if (!m_history.empty()) { // 返回前一个状态的备忘录
87
return m_history.back();
88
} else {
89
// 没有更多的历史状态,返回空备忘录或代表初始状态的备忘录
90
return nullptr; // 示例中简单返回nullptr
91
}
92
} else {
93
std::cout << "没有历史记录可以撤销。" << std::endl;
94
return nullptr;
95
}
96
}
97
98
// Caretaker 不能访问 Memento 的内部状态,例如:
99
// void inspectMemento(std::shared_ptr<EditorMemento> memento) {
100
// // 错误:无法访问 memento->getContent()
101
// // std::cout << "Memento content: " << memento->getContent() << std::endl;
102
// }
103
};
104
105
106
// 客户端代码 (Client)
107
int main() {
108
auto editor = std::make_shared<EditorOriginator>("初始文本。");
109
EditorCaretaker caretaker;
110
111
// 输入一些文本并保存状态
112
caretaker.save(editor->createMemento()); // 保存初始状态
113
editor->type("输入第一段。");
114
115
caretaker.save(editor->createMemento()); // 保存状态 1
116
editor->type("输入第二段。");
117
118
caretaker.save(editor->createMemento()); // 保存状态 2
119
editor->type("输入第三段。");
120
121
std::cout << "\n--- 执行撤销 ---" << std::endl;
122
123
// 执行撤销
124
std::shared_ptr<EditorMemento> memento1 = caretaker.undo();
125
editor->restoreMemento(memento1); // 恢复到状态 2
126
127
std::shared_ptr<EditorMemento> memento2 = caretaker.undo();
128
editor->restoreMemento(memento2); // 恢复到状态 1
129
130
std::shared_ptr<EditorMemento> memento3 = caretaker.undo();
131
// 恢复到初始状态(在 undo 方法中取出了状态 1 的备忘录,下次 undo 返回的是初始状态的备忘录)
132
editor->restoreMemento(memento3); // 恢复到初始状态
133
134
std::shared_ptr<EditorMemento> memento4 = caretaker.undo(); // 没有更多历史记录
135
editor->restoreMemento(memento4);
136
137
return 0;
138
}
在这个例子中,EditorOriginator
是 Originator
,它包含需要保存和恢复的文本内容。EditorMemento
是 Memento
,它存储 EditorOriginator
的文本内容。我们将 EditorMemento
的构造函数和 getContent
方法设为私有,并通过 friend class EditorOriginator;
声明让 EditorOriginator
可以访问这些私有成员,从而保护了 Memento 的封装性,只有 Originator 知道如何存取备忘录中的状态。EditorCaretaker
是 Caretaker
,它维护一个备忘录的 std::vector
作为历史记录,并提供 save
和 undo
方法。Caretaker 只能保存和取出 Memento 对象,不能访问其内部内容,符合模式的要求。客户端通过调用 createMemento
和 restoreMemento
方法,并由 Caretaker 管理备忘录,实现了撤销功能。注意,undo()
方法需要返回用于恢复的备忘录,并且在实际实现中,Caretaker 的 undo 逻辑可能更复杂,比如维护两个栈分别用于 undo 和 redo。这里为了简化,只实现了简单的 undo 逻辑。
适用性 (Applicability)
备忘录模式适用于以下情况:
⚝ 需要保存一个对象在某个时刻的状态,以便在未来某个时刻恢复到该状态。
⚝ 保存状态的操作需要隔离,不应该破坏对象的封装性。
⚝ 使用直接接口保存状态的开销或复杂性太高。
相关模式 (Related Patterns)
⚝ 命令模式 (Command Pattern): 命令模式可以与备忘录模式结合使用,实现可撤销的命令。每个命令对象可以在执行前创建 Originator 的备忘录,并在撤销时使用该备忘录恢复状态。
⚝ 状态模式 (State Pattern): 备忘录模式可以用于保存状态对象 (State Object) 或上下文对象 (Context Object) 的状态,以便在需要时回退到之前的状态。
5.6 观察者模式 (Observer Pattern)
意图 (Intent)
定义对象间的一种一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
动机 (Motivation)
假设您正在开发一个股票交易系统。系统中有多种组件需要关注股票价格的变化:比如一个显示股票价格的图表、一个显示交易历史的列表、一个根据价格变动触发交易指令的模块等等。当某只股票的价格发生变化时,所有这些关注该股票的组件都需要立即得到通知并更新自身。
最直接的方法可能是股票对象在价格变化时,直接调用所有相关组件的更新方法。但这样做会导致股票对象与所有具体组件类型紧密耦合。如果需要添加新的关注者,或者移除现有的关注者,股票对象的代码就需要修改,这违反了开放/封闭原则,并且使得系统难以维护和扩展。
观察者模式提供了一种灵活的解决方案:引入“主题” (Subject) 和“观察者” (Observer) 的概念。主题是被观察的对象,它维护一个观察者列表,当其状态发生变化时,就通知所有注册的观察者。观察者是那些依赖于主题状态的对象,它们向主题注册自己,以便在主题状态变化时接收通知。主题和观察者之间的关系是松散耦合的:主题只知道它有一系列观察者,这些观察者实现了共同的更新接口,但不知道它们的具体类型;观察者知道它们观察哪个主题,但不知道其他观察者的存在。
结构 (Structure)
模式的关键参与者包括:
⚝ Subject (主题):
▮▮▮▮⚝ 维护一系列 Observer 对象的引用。
▮▮▮▮⚝ 提供接口用于附加 (attach()
) 和分离 (detach()
) Observer 对象。
▮▮▮▮⚝ 在状态发生改变时,调用其 Observer 的 update()
方法通知所有注册的观察者。
⚝ Observer (观察者):
▮▮▮▮⚝ 定义一个更新接口,供 Subject 在其状态发生改变时通知它。通常包含一个 update()
方法。
⚝ ConcreteSubject (具体主题):
▮▮▮▮⚝ 维护对其状态感兴趣的 ConcreteObserver 对象的列表。
▮▮▮▮⚝ 将自身状态的变化通知给它的 Observer。
▮▮▮▮⚝ 存储对观察者很重要的状态。
⚝ ConcreteObserver (具体观察者):
▮▮▮▮⚝ 维护一个 ConcreteSubject 对象的引用。
▮▮▮▮⚝ 实现 Observer 的更新接口 (update()
),以使自身状态与主题的状态保持一致。
▮▮▮▮⚝ 可能需要在 update()
方法中从 Subject 获取状态。
一个简化的UML类图结构(文本描述):
1
graph LR
2
Subject --> Observer
3
Subject <|-- ConcreteSubject
4
Observer <|-- ConcreteObserver
5
ConcreteSubject -- notify --> Observer: update()
6
ConcreteObserver --> ConcreteSubject: getStatus()
协作 (Collaboration)
① ConcreteObserver 对象向 ConcreteSubject 对象注册自己(调用 attach()
)。
② ConcreteSubject 的状态发生变化。
③ ConcreteSubject 通过调用其注册的每个 Observer 的 update()
方法通知所有观察者。
④ ConcreteObserver 接收到通知后,如果需要详细信息,可以调用 ConcreteSubject 的方法(例如 getStatus()
)来获取主题的最新状态,然后更新自身。这种方式称为“拉模型” (Pull Model),观察者主动从主题拉取数据。另一种方式是“推模型” (Push Model),主题在通知时直接将相关数据传递给观察者,观察者直接使用这些数据更新。拉模型更灵活,但可能需要观察者与具体主题耦合;推模型更直接,但主题需要知道观察者需要哪些数据。
优点 (Advantages)
⚝ 主题和观察者之间的松散耦合 (Loose Coupling): 主题和观察者可以独立地变化。主题无需知道观察者的具体类型,只需知道它们实现了 Observer 接口。观察者无需知道其他观察者的存在。
⚝ 支持广播通信 (Support for Broadcast Communication): 主题可以同时通知所有感兴趣的观察者。
⚝ 易于增加新的观察者 (Ease of Adding New Observers): 添加新的观察者只需实现 Observer 接口并向主题注册即可,无需修改主题或现有观察者代码。
缺点 (Disadvantages)
⚝ 可能导致意外的更新 (Potential for Unexpected Updates): 如果一个主题有很多观察者,并且某些观察者的 update()
方法执行时间较长或逻辑复杂,可能会影响整个系统的性能或行为。
⚝ 更新顺序不确定 (Undetermined Update Order): 在简单的实现中,主题通常按注册顺序通知观察者,但并不能保证特定的通知顺序,这可能在某些情况下造成问题。
⚝ 通知开销 (Notification Overhead): 如果主题状态变化频繁,通知大量观察者可能会产生显著的性能开销。
⚝ 循环引用问题 (Potential for Cyclic References): 在某些实现中(尤其是在没有垃圾回收的C++中),如果主题和观察者相互持有强引用(例如 std::shared_ptr
),可能导致循环引用和内存泄漏。可以使用弱引用 (std::weak_ptr
) 来避免这个问题。
C++实现示例 (C++ Implementation Example)
模拟一个简单的股票价格监控系统。
1
#include <iostream>
2
#include <vector>
3
#include <string>
4
#include <memory> // For std::shared_ptr, std::weak_ptr
5
#include <algorithm> // For std::remove_if
6
7
// 前向声明 Subject 和 Observer
8
class Subject;
9
class Observer;
10
11
// 抽象观察者接口 (Observer)
12
class Observer {
13
public:
14
virtual ~Observer() = default;
15
// update 方法,当主题状态变化时被调用
16
// 可以根据是推模型还是拉模型决定参数
17
// 推模型示例: virtual void update(double newPrice) = 0;
18
// 拉模型示例:
19
virtual void update() = 0;
20
21
// 对于拉模型,观察者可能需要知道它观察的是哪个主题
22
virtual std::shared_ptr<Subject> getSubject() const = 0;
23
};
24
25
// 抽象主题接口 (Subject)
26
class Subject : public std::enable_shared_from_this<Subject> {
27
protected:
28
// 使用 weak_ptr 管理观察者,避免循环引用
29
std::vector<std::weak_ptr<Observer>> m_observers;
30
31
public:
32
virtual ~Subject() = default;
33
34
// 附加观察者 (Attach)
35
void attach(std::shared_ptr<Observer> observer) {
36
// 在添加前检查观察者是否已经存在,避免重复添加
37
bool found = false;
38
for(const auto& weak_obs : m_observers) {
39
if (auto obs = weak_obs.lock()) {
40
if (obs == observer) {
41
found = true;
42
break;
43
}
44
}
45
}
46
if (!found) {
47
m_observers.push_back(observer);
48
std::cout << "主题: 附加了新的观察者。" << std::endl;
49
} else {
50
std::cout << "主题: 观察者已存在,无需重复附加。" << std::endl;
51
}
52
}
53
54
// 分离观察者 (Detach)
55
void detach(std::shared_ptr<Observer> observer) {
56
m_observers.erase(
57
std::remove_if(m_observers.begin(), m_observers.end(),
58
[&](const std::weak_ptr<Observer>& weak_obs) {
59
if (auto obs = weak_obs.lock()) {
60
return obs == observer;
61
}
62
return true; // 如果 weak_ptr 失效了,也移除
63
}),
64
m_observers.end()
65
);
66
std::cout << "主题: 分离了观察者。" << std::endl;
67
}
68
69
// 通知观察者 (Notify)
70
void notify() {
71
// 在通知前清理无效的 weak_ptr
72
m_observers.erase(
73
std::remove_if(m_observers.begin(), m_observers.end(),
74
[](const std::weak_ptr<Observer>& weak_obs) {
75
return weak_obs.expired(); // 移除已失效的观察者
76
}),
77
m_observers.end()
78
);
79
80
std::cout << "主题: 通知所有观察者..." << std::endl;
81
for (const auto& weak_obs : m_observers) {
82
if (auto obs = weak_obs.lock()) { // 尝试获取 shared_ptr
83
obs->update(); // 调用观察者的更新方法
84
}
85
}
86
}
87
};
88
89
// 具体主题 (ConcreteSubject):股票
90
class Stock : public Subject {
91
private:
92
std::string m_symbol;
93
double m_price;
94
95
public:
96
Stock(const std::string& symbol, double price) :
97
m_symbol(symbol), m_price(price) {}
98
99
// 获取股票代码和价格 (供拉模型观察者使用)
100
const std::string& getSymbol() const { return m_symbol; }
101
double getPrice() const { return m_price; }
102
103
// 设置股票价格并通知观察者
104
void setPrice(double newPrice) {
105
std::cout << "\n股票 [" << m_symbol << "] 价格更新: " << m_price << " -> " << newPrice << std::endl;
106
if (m_price != newPrice) {
107
m_price = newPrice;
108
notify(); // 通知所有观察者
109
}
110
}
111
};
112
113
// 具体观察者 (ConcreteObserver):股票图表
114
class StockChart : public Observer, public std::enable_shared_from_this<StockChart> {
115
private:
116
std::weak_ptr<Subject> m_subject; // 观察的主题 (weak_ptr 避免循环引用)
117
118
public:
119
StockChart(std::shared_ptr<Subject> subject) : m_subject(subject) {
120
// 在构造函数中向主题注册自己
121
if (auto s = m_subject.lock()) {
122
s->attach(shared_from_this());
123
}
124
}
125
126
~StockChart() {
127
// 在析构函数中从主题分离自己
128
if (auto s = m_subject.lock()) {
129
s->detach(shared_from_this());
130
}
131
std::cout << "股票图表对象已销毁,从主题分离。" << std::endl;
132
}
133
134
void update() override {
135
// 拉模型:从主题获取最新状态
136
if (auto s = m_subject.lock()) {
137
if (auto stock = std::dynamic_pointer_cast<Stock>(s)) { // 动态转换到具体主题类型
138
std::cout << "[股票图表] 收到更新通知,当前股票 [" << stock->getSymbol()
139
<< "] 价格: " << stock->getPrice() << "。更新图表..." << std::endl;
140
}
141
}
142
}
143
144
std::shared_ptr<Subject> getSubject() const override { return m_subject.lock(); }
145
};
146
147
// 具体观察者 (ConcreteObserver):交易列表
148
class TradeList : public Observer, public std::enable_shared_from_this<TradeList> {
149
private:
150
std::weak_ptr<Subject> m_subject; // 观察的主题
151
152
public:
153
TradeList(std::shared_ptr<Subject> subject) : m_subject(subject) {
154
if (auto s = m_subject.lock()) {
155
s->attach(shared_from_this());
156
}
157
}
158
159
~TradeList() {
160
if (auto s = m_subject.lock()) {
161
s->detach(shared_from_this());
162
}
163
std::cout << "交易列表对象已销毁,从主题分离。" << std::endl;
164
}
165
166
void update() override {
167
// 拉模型:从主题获取最新状态
168
if (auto s = m_subject.lock()) {
169
if (auto stock = std::dynamic_pointer_cast<Stock>(s)) {
170
std::cout << "[交易列表] 收到更新通知,当前股票 [" << stock->getSymbol()
171
<< "] 价格: " << stock->getPrice() << "。更新交易记录..." << std::endl;
172
}
173
}
174
}
175
176
std::shared_ptr<Subject> getSubject() const override { return m_subject.lock(); }
177
};
178
179
180
// 客户端代码 (Client)
181
int main() {
182
// 创建主题
183
auto appleStock = std::make_shared<Stock>("AAPL", 150.00);
184
185
// 创建观察者
186
auto chart = std::make_shared<StockChart>(appleStock); // 构造时自动附加
187
auto tradeList = std::make_shared<TradeList>(appleStock); // 构造时自动附加
188
189
std::cout << "\n--- 模拟股票价格变化 ---" << std::endl;
190
191
// 模拟股票价格变化
192
appleStock->setPrice(152.50); // 通知观察者
193
194
std::cout << std::endl;
195
196
appleStock->setPrice(151.00); // 通知观察者
197
198
std::cout << std::endl;
199
200
// 模拟一个观察者对象被销毁 (离开作用域或 reset)
201
std::cout << "--- 销毁一个观察者 ---" << std::endl;
202
chart.reset(); // StockChart 析构时会自动从 Subject 分离
203
204
std::cout << "\n--- 再次模拟股票价格变化 ---" << std::endl;
205
206
appleStock->setPrice(153.75); // 只有 TradeList 会收到通知
207
208
std::cout << std::endl;
209
210
// 所有智能指针离开作用域,对象被销毁
211
appleStock.reset();
212
tradeList.reset();
213
214
// 注意:由于使用了 enable_shared_from_this 和 weak_ptr,循环引用问题得到了解决。
215
// 对象的生命周期由 shared_ptr 管理。当所有 shared_ptr 都被 reset 或离开作用域,对象就会被正确销毁。
216
// weak_ptr 允许观察者引用主题而不会阻止主题被销毁,反之亦然。主题在 notify 时会自动清理已失效的 weak_ptr。
217
218
return 0;
219
}
在这个C++实现中,Subject
和 Observer
是抽象基类。Stock
是 ConcreteSubject
,它维护股票价格和观察者列表。观察者列表使用 std::vector<std::weak_ptr<Observer>>
来存储,这是避免循环引用的一种常见C++实践:主题持有观察者的 weak_ptr
,观察者持有主题的 weak_ptr
,或者观察者只持有主题的 shared_ptr
而主题持有观察者的 weak_ptr
(本例采用后一种)。StockChart
和 TradeList
是 ConcreteObserver
,它们在构造时向 Stock
附加自己,并在析构时分离。当 Stock
的价格变化时,它调用 notify()
方法,遍历观察者列表,并通过 weak_ptr::lock()
尝试获取有效的 shared_ptr
,然后调用观察者的 update()
方法。本例采用“拉模型”,观察者在 update()
中通过调用主题的 getSymbol()
和 getPrice()
方法来获取所需的状态信息。使用 std::enable_shared_from_this
允许对象在成员函数中安全地获取自身的 shared_ptr
,这在需要将自身的 shared_ptr
传递给其他对象(例如在构造观察者时注册)时非常有用。
5.6.1 基于回调函数或信号槽的实现 (Callback or Signal/Slot based Implementations)
除了经典的面向对象实现,观察者模式在C++中还可以通过其他机制实现,这些机制通常更轻量级或提供了更强大的功能:
① 基于回调函数 (Callback Functions):
▮▮▮▮⚝ 主题不维护观察者对象的列表,而是维护一个函数指针 (Function Pointer)、函数对象 (Function Object) 或 std::function
的列表。
▮▮▮▮⚝ 观察者(或其他客户端代码)注册一个回调函数或函数对象到主题。
▮▮▮▮⚝ 当主题状态改变时,它依次调用注册列表中的函数。
▮▮▮▮⚝ 优点是实现简单,无需定义单独的 Observer 类。缺点是回调函数通常是全局函数或静态成员函数,难以访问具体对象的成员;或者需要通过 std::bind
或 Lambda 表达式捕获对象指针,需要注意生命周期管理。
1
#include <iostream>
2
#include <vector>
3
#include <string>
4
#include <functional> // For std::function
5
6
// 基于回调函数的主题
7
class CallbackSubject {
8
public:
9
using Callback = std::function<void(double newPrice)>; // 定义回调函数类型
10
11
void registerCallback(Callback callback) {
12
m_callbacks.push_back(callback);
13
std::cout << "CallbackSubject: 注册了一个回调函数。" << std::endl;
14
}
15
16
void notify(double newPrice) {
17
std::cout << "CallbackSubject: 通知所有回调函数,新价格: " << newPrice << std::endl;
18
for (const auto& cb : m_callbacks) {
19
cb(newPrice); // 调用回调函数
20
}
21
}
22
23
private:
24
std::vector<Callback> m_callbacks; // 回调函数列表
25
};
26
27
// 示例观察者(使用 Lambda 捕获自身)
28
class MyObserver {
29
private:
30
std::string m_name;
31
public:
32
MyObserver(const std::string& name) : m_name(name) {}
33
34
void reactToPriceChange(double price) {
35
std::cout << "[" << m_name << "] 收到价格变化通知,新价格: " << price << std::endl;
36
}
37
38
// 注册方法,使用 Lambda 表达式捕获 this
39
CallbackSubject::Callback getPriceChangeCallback() {
40
return [this](double price) {
41
this->reactToPriceChange(price);
42
};
43
}
44
};
45
46
int main() {
47
CallbackSubject subject;
48
MyObserver obs1("Observer A");
49
MyObserver obs2("Observer B");
50
51
subject.registerCallback(obs1.getPriceChangeCallback());
52
subject.registerCallback(obs2.getPriceChangeCallback());
53
54
subject.notify(200.0);
55
subject.notify(205.5);
56
57
return 0;
58
}
这种方式更灵活,特别是结合 C++11 引入的 Lambda 表达式和 std::function
,可以方便地注册成员函数作为回调。但需要谨慎处理对象的生命周期,防止 Lambda 捕获的指针或引用在回调被触发时已经失效(即悬空问题)。
② 基于信号槽机制 (Signal/Slot Mechanism):
▮▮▮▮⚝ 信号槽是一种更强大的基于回调的机制,通常由特定的库提供,如 Qt, Boost.Signals2 等。
▮▮▮▮⚝ “信号” (Signal) 类似于主题的状态变化事件。
▮▮▮▮⚝ “槽” (Slot) 是观察者中用于响应信号的方法(回调函数)。
▮▮▮▮⚝ 通过 connect
操作将信号与槽连接起来,实现观察者注册。
▮▮▮▮⚝ 信号槽机制通常提供更安全和便捷的连接/断开机制,并能处理多对多关系。有些实现还支持队列连接,可以在不同线程之间进行通知。
▮▮▮▮⚝ 优点是类型安全,连接/断开方便,可以很方便地将多个信号连接到一个槽,或一个信号连接到多个槽。某些库提供了自动连接管理,有助于避免悬空槽。缺点是需要依赖特定的库。
1
// 以下是一个概念性示例,实际需要依赖如 Qt 或 Boost.Signals2 库
2
/*
3
#include <QObject> // Qt 示例
4
#include <iostream>
5
6
// Qt 中的主题
7
class Stock : public QObject {
8
Q_OBJECT // 必须有的宏
9
10
signals:
11
void priceChanged(double newPrice); // 定义信号
12
13
public:
14
Stock(QObject* parent = nullptr) : QObject(parent), m_price(0.0) {}
15
16
void setPrice(double newPrice) {
17
if (m_price != newPrice) {
18
m_price = newPrice;
19
emit priceChanged(newPrice); // 发射信号
20
}
21
}
22
private:
23
double m_price;
24
};
25
26
// Qt 中的观察者
27
class StockWatcher : public QObject {
28
Q_OBJECT // 必须有的宏
29
30
public slots: // 定义槽
31
void onPriceChanged(double price) {
32
std::cout << "收到价格变化通知,新价格: " << price << std::endl;
33
}
34
};
35
36
int main() {
37
Stock appleStock;
38
StockWatcher watcher1, watcher2;
39
40
// 连接信号和槽
41
QObject::connect(&appleStock, &Stock::priceChanged,
42
&watcher1, &StockWatcher::onPriceChanged);
43
QObject::connect(&appleStock, &Stock::priceChanged,
44
&watcher2, &StockWatcher::onPriceChanged);
45
46
appleStock.setPrice(150.0);
47
appleStock.setPrice(155.5);
48
49
// 销毁观察者会自动断开连接 (取决于库的实现)
50
// delete &watcher1; // 示例
51
52
return 0;
53
}
54
*/
信号槽机制是观察者模式的一种更高级和实用的变体,广泛应用于GUI框架和其他事件驱动系统中。
总之,在C++中实现观察者模式有多种方式,经典的面向对象方式提供了清晰的角色划分,基于回调函数的方式更加轻量灵活,而基于信号槽的库则提供了更强大和安全的事件处理能力。选择哪种方式取决于具体的需求、项目的复杂度和对库的依赖程度。对于需要避免循环引用和管理生命周期的场景,使用智能指针 (特别是 weak_ptr
) 或依赖提供自动连接管理的信号槽库是推荐的做法。
5.7 状态模式 (State Pattern)
意图 (Intent)
允许对象在内部状态改变时改变其行为。对象看起来好像修改了它的类。
动机 (Motivation)
考虑一个网络连接对象,它可能处于不同的状态:建立连接 (Establishing)、已连接 (Connected)、断开连接 (Disconnected) 等。在不同的状态下,相同的操作(例如发送数据包、关闭连接)可能会产生不同的行为。例如,在已连接状态下发送数据包会成功;在断开连接状态下发送数据包则会失败并可能抛出异常。
如果使用传统的条件语句 (如 if-else
或 switch-case
) 来处理不同状态下的行为,代码可能会变成这样:
1
class NetworkConnection {
2
private:
3
enum State { ESTABLISHING, CONNECTED, DISCONNECTED } m_state;
4
5
public:
6
void sendData(const Data& data) {
7
switch (m_state) {
8
case ESTABLISHING:
9
// 暂存数据或报错
10
break;
11
case CONNECTED:
12
// 真正发送数据
13
break;
14
case DISCONNECTED:
15
// 报错
16
break;
17
}
18
}
19
20
void close() {
21
switch (m_state) {
22
case ESTABLISHING:
23
case CONNECTED:
24
// 执行关闭逻辑
25
m_state = DISCONNECTED;
26
break;
27
case DISCONNECTED:
28
// 已经断开了,什么也不做或报错
29
break;
30
}
31
}
32
// ... 其他操作,每个操作都需要检查状态 ...
33
};
这种方式会导致:
① 包含大量条件语句,代码结构混乱,难以阅读和维护。
② 随着状态和操作的增加,条件判断会呈指数级增长。
③ 每当增加新的状态或修改某个状态的行为时,都需要修改多个方法中的条件判断,违反了开放/封闭原则。
④ 状态转换逻辑散布在各个操作方法中,难以集中管理。
状态模式提供了一种将对象行为与状态分离的解决方案。它将每一种状态封装成一个独立的类,并将特定状态下的行为委托给代表该状态的对象。原对象(称为上下文 Context)不再维护一个表示当前状态的枚举或变量,而是持有一个指向当前状态对象的引用。当上下文对象收到请求时,它将请求委托给其当前状态对象来处理。状态对象处理请求,并在必要时改变上下文对象所持有的状态对象,从而实现状态的转换。
结构 (Structure)
模式的关键参与者包括:
⚝ Context (上下文):
▮▮▮▮⚝ 定义客户端感兴趣的接口。
▮▮▮▮⚝ 维护一个 ConcreteState 子类的实例,这个实例定义了对象的当前状态。
▮▮▮▮⚝ 将与其状态相关的请求委托给当前 State 对象。
▮▮▮▮⚝ 可能提供接口供 State 对象改变其当前状态。
⚝ State (状态):
▮▮▮▮⚝ 定义一个接口,封装了 Context 的一个特定状态相关的行为。
⚝ ConcreteState (具体状态):
▮▮▮▮⚝ 实现 State 接口。
▮▮▮▮⚝ 实现特定于该状态的行为。
▮▮▮▮⚝ 可能在执行自身行为后,改变 Context 的状态(通过调用 Context 的方法)。
一个简化的UML类图结构(文本描述):
1
graph LR
2
Client --> Context
3
Context --> State: currentState
4
State <|-- ConcreteStateA
5
State <|-- ConcreteStateB
6
ConcreteStateA --> Context: changeState()
7
ConcreteStateB --> Context: changeState()
协作 (Collaboration)
① 客户端创建 Context 对象,并可能设置其初始状态。
② 客户端向 Context 对象发送请求。
③ Context 对象将请求委托给其当前持有的 State 对象来处理。
④ ConcreteState 对象执行特定于该状态的行为。
⑤ 在某些情况下,ConcreteState 对象可能会决定改变 Context 的状态,通过调用 Context 提供的方法来切换到另一个 ConcreteState 实例。
优点 (Advantages)
⚝ 将特定于状态的行为局部化 (Localizes State-Specific Behavior): 每种状态的行为都封装在一个独立的类中,使得代码更加模块化和易于理解。
⚝ 消除了大量的条件语句 (Eliminates Large Conditional Statements): 将基于状态的条件逻辑替换为多态调用,简化了 Context 类的代码。
⚝ 简化了状态转换 (Simplifies State Transitions): 状态转换逻辑被封装在 State 子类中,而不是散布在 Context 中,使得状态转换关系更加清晰和易于维护。
⚝ 使得增加新状态变得容易 (Makes Adding New States Easy): 增加新状态只需创建新的 ConcreteState 类,并可能在现有 State 类中增加到新状态的转换逻辑,无需修改 Context 或其他 State 类(符合开放/封闭原则)。
缺点 (Disadvantages)
⚝ 增加了类的数量 (Increased Number of Classes): 每一种状态通常都需要一个独立的类,当状态数量很多时,会导致类数量膨胀。
⚝ 可能引入额外的复杂性 (Potential for Increased Complexity): 对于只有少量状态且转换简单的场景,引入状态模式可能显得过度设计。
C++实现示例 (C++ Implementation Example)
模拟一个简单的电梯状态控制。电梯有“停止”、“运行中”两个状态,并支持“开门”、“关门”、“上行”、“下行”等操作。
1
#include <iostream>
2
#include <memory> // For std::shared_ptr
3
4
// 前向声明 Context
5
class Elevator;
6
7
// 抽象状态接口 (State)
8
class ElevatorState {
9
public:
10
virtual ~ElevatorState() = default;
11
// 定义不同状态下可能的操作
12
virtual void openDoor(Elevator* elevator) = 0;
13
virtual void closeDoor(Elevator* elevator) = 0;
14
virtual void goUp(Elevator* elevator) = 0;
15
virtual void goDown(Elevator* elevator) = 0;
16
};
17
18
// Context (上下文)
19
class Elevator : public std::enable_shared_from_this<Elevator> {
20
private:
21
std::shared_ptr<ElevatorState> m_currentState; // 当前状态
22
int m_currentFloor; // 电梯当前楼层
23
24
public:
25
Elevator(int initialFloor = 1); // 在实现中设置初始状态
26
27
// 设置当前状态 (供状态对象调用)
28
void setState(std::shared_ptr<ElevatorState> state) {
29
m_currentState = state;
30
}
31
32
int getCurrentFloor() const { return m_currentFloor; }
33
void setCurrentFloor(int floor) { m_currentFloor = floor; }
34
35
// 客户端调用的操作,委托给当前状态对象处理
36
void openDoor() { m_currentState->openDoor(this); }
37
void closeDoor() { m_currentState->closeDoor(this); }
38
void goUp() { m_currentState->goUp(this); }
39
void goDown() { m_currentState->goDown(this); }
40
};
41
42
// 具体状态 (ConcreteState):停止状态
43
class StoppedState : public ElevatorState, public std::enable_shared_from_this<StoppedState> {
44
public:
45
static std::shared_ptr<ElevatorState> getInstance() {
46
static auto instance = std::make_shared<StoppedState>();
47
return instance;
48
}
49
50
void openDoor(Elevator* elevator) override {
51
std::cout << "电梯停止中,打开门。" << std::endl;
52
// 停止状态下可以开门
53
// 状态不变,或者进入 "门开着状态" (取决于更细粒度的状态划分)
54
}
55
56
void closeDoor(Elevator* elevator) override {
57
std::cout << "电梯停止中,关闭门。" << std::endl;
58
// 停止状态下可以关门
59
// 状态不变
60
}
61
62
void goUp(Elevator* elevator) override {
63
std::cout << "电梯停止中,开始上行。" << std::endl;
64
// 停止状态转换到运行状态
65
elevator->setState(RunningState::getInstance());
66
}
67
68
void goDown(Elevator* elevator) override {
69
std::cout << "电梯停止中,开始下行。" << std::endl;
70
// 停止状态转换到运行状态
71
elevator->setState(RunningState::getInstance());
72
}
73
};
74
75
// 具体状态 (ConcreteState):运行中状态
76
class RunningState : public ElevatorState, public std::enable_shared_from_this<RunningState> {
77
public:
78
static std::shared_ptr<ElevatorState> getInstance() {
79
static auto instance = std::make_shared<RunningState>();
80
return instance;
81
}
82
83
void openDoor(Elevator* elevator) override {
84
std::cout << "电梯运行中,无法打开门。" << std::endl;
85
// 运行中无法开门
86
}
87
88
void closeDoor(Elevator* elevator) override {
89
std::cout << "电梯运行中,门已关闭。" << std::endl;
90
// 运行中门本来就应该关闭
91
}
92
93
void goUp(Elevator* elevator) override {
94
std::cout << "电梯运行中,继续上行。" << std::endl;
95
// 运行中继续运行
96
}
97
98
void goDown(Elevator* elevator) override {
99
std::cout << "电梯运行中,继续下行。" << std::endl;
100
// 运行中继续运行
101
}
102
103
// 运行中状态下可能发生的其他事件,比如到达楼层
104
void arriveAtFloor(Elevator* elevator, int floor) {
105
std::cout << "电梯到达 " << floor << " 层。" << std::endl;
106
elevator->setCurrentFloor(floor);
107
// 到达楼层后转换到停止状态
108
elevator->setState(StoppedState::getInstance());
109
}
110
};
111
112
// 静态成员需要在类定义外部实现 (这里使用单例模式的静态方法获取状态实例)
113
std::shared_ptr<ElevatorState> StoppedState::instance;
114
std::shared_ptr<ElevatorState> RunningState::instance;
115
116
117
// 实现 Context 的构造函数
118
Elevator::Elevator(int initialFloor) : m_currentFloor(initialFloor) {
119
// 初始状态设置为停止状态
120
m_currentState = StoppedState::getInstance();
121
std::cout << "电梯初始化完成,当前在 " << m_currentFloor << " 层,状态: 停止。" << std::endl;
122
}
123
124
125
// 客户端代码 (Client)
126
int main() {
127
auto elevator = std::make_shared<Elevator>(5); // 电梯在5层,初始停止
128
129
std::cout << "\n--- 模拟操作 ---" << std::endl;
130
131
elevator->openDoor(); // 停止状态下可以开门
132
elevator->closeDoor(); // 停止状态下可以关门
133
134
elevator->goUp(); // 停止状态下上行 -> 运行中
135
elevator->openDoor(); // 运行中状态下开门 (无效)
136
elevator->goDown(); // 运行中状态下下行 (继续运行)
137
138
// 模拟到达楼层 (这通常是电梯内部触发的事件,这里为演示直接调用)
139
auto runningState = std::dynamic_pointer_cast<RunningState>(elevator->m_currentState);
140
if (runningState) {
141
runningState->arriveAtFloor(elevator.get(), 10); // 到达10层 -> 停止
142
}
143
144
elevator->openDoor(); // 停止状态下开门 (有效)
145
146
std::cout << "\n--- 操作结束 ---" << std::endl;
147
148
return 0;
149
}
在这个例子中,Elevator
是 Context
,它持有一个 ElevatorState
类型的 m_currentState
指针,并将其操作(openDoor
, closeDoor
等)委托给 m_currentState
。ElevatorState
是抽象状态接口。StoppedState
和 RunningState
是 ConcreteState
类,它们实现了 ElevatorState
的接口,并在各自的方法中定义了在该状态下对应操作的行为。例如,StoppedState::openDoor
打印“电梯停止中,打开门”,而 RunningState::openDoor
打印“电梯运行中,无法打开门”。状态转换逻辑也封装在具体状态类中,比如 StoppedState::goUp
和 goDown
会将 Elevator
的状态设置为 RunningState
。为了避免重复创建状态对象,我们使用了简单的单例模式来获取 StoppedState
和 RunningState
的实例。注意,在C++中,状态对象通常会持有指向 Context 的指针(这里是原始指针 Elevator*
)以便在需要时改变 Context 的状态,或者调用 Context 的其他方法。同样需要注意生命周期管理,这里 Context 持有 State 的 shared_ptr
,而 State 持有 Context 的原始指针,可以避免循环引用。
适用性 (Applicability)
状态模式适用于以下情况:
⚝ 一个对象的行为取决于其状态,并且它在运行时可以改变它的状态。
⚝ 一个操作中包含大量的条件语句,这些条件依赖于对象的状态。
⚝ 定义一个对象状态机,其中状态和转换规则复杂且相互依赖。
相关模式 (Related Patterns)
⚝ 策略模式 (Strategy Pattern): 状态模式和策略模式的结构非常相似,都使用继承和多态将不同的行为封装到独立的类中,并由 Context 对象持有这些行为对象的引用。它们之间的区别在于意图:策略模式关注算法的可替换性,行为之间通常没有相互关联,客户端通常可以指定使用哪个策略;而状态模式关注对象状态的变化,行为是与当前状态紧密相关的,状态之间有明确的转换关系,状态转换通常由状态对象或 Context 自己控制,客户端通常无需关心或指定具体状态。状态模式的行为是固定的状态机的一部分,策略模式的行为是可插拔的算法。
⚝ 工厂方法模式 (Factory Method Pattern): Context 可以使用工厂方法来创建其状态对象,从而将状态对象的创建与 Context 类解耦。
5.8 策略模式 (Strategy Pattern)
意图 (Intent)
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
动机 (Motivation)
假设您正在开发一个电子商务系统,需要为客户提供多种不同的支付方式:例如信用卡支付、借记卡支付、支付宝支付、微信支付等。每种支付方式都有其特定的处理流程。如果在一个订单处理类中,使用条件语句 (如 if-else
或 switch-case
) 来根据用户选择的支付方式执行相应的逻辑,代码可能会变得臃肿且难以维护:
1
class OrderProcessor {
2
// ...
3
void processPayment(PaymentMethod method, double amount) {
4
switch (method) {
5
case CREDIT_CARD:
6
// 处理信用卡支付逻辑
7
break;
8
case DEBIT_CARD:
9
// 处理借记卡支付逻辑
10
break;
11
case ALIPAY:
12
// 处理支付宝支付逻辑
13
break;
14
case WECHAT_PAY:
15
// 处理微信支付逻辑
16
break;
17
}
18
}
19
// ...
20
};
这种方式的缺点与状态模式类似:
① 代码中包含大量的条件判断。
② 增加新的支付方式时,需要修改 processPayment
方法,违反了开放/封闭原则。
③ 支付算法与使用它们的类紧密耦合,难以复用。
策略模式提供了一种将算法与其使用环境分离的解决方案。它将每一种算法(支付方式)封装成一个独立的类,这些类都实现一个共同的接口。使用算法的类(称为上下文 Context)不再直接实现算法,而是持有一个指向当前策略对象 (Strategy) 的引用。当 Context 需要执行某个算法时,它将请求委托给其当前持有的 Strategy 对象来处理。客户端可以根据需要选择并配置 Context 使用的 Strategy 对象,从而在运行时改变 Context 的行为。
结构 (Structure)
模式的关键参与者包括:
⚝ Strategy (策略):
▮▮▮▮⚝ 定义所有支持的算法的公共接口。Context 使用这个接口来调用由 ConcreteStrategy 实现的算法。
⚝ ConcreteStrategy (具体策略):
▮▮▮▮⚝ 实现 Strategy 接口,提供具体的算法实现。
⚝ Context (上下文):
▮▮▮▮⚝ 维护一个 Strategy 对象的引用。
▮▮▮▮⚝ 配置一个 ConcreteStrategy 对象。
▮▮▮▮⚝ 将其客户端的请求委托给其 Strategy 对象来处理。
一个简化的UML类图结构(文本描述):
1
graph LR
2
Client --> Context
3
Client --> Strategy
4
Context --> Strategy: currentStrategy
5
Strategy <|-- ConcreteStrategyA
6
Strategy <|-- ConcreteStrategyB
注意:与状态模式不同,策略模式中客户端通常负责创建 ConcreteStrategy 并将其配置给 Context。行为(算法)之间的切换通常由客户端触发,而不是由策略对象自身。
协作 (Collaboration)
① 客户端创建 Context 对象和 ConcreteStrategy 对象。
② 客户端配置 Context 对象,让其引用一个 ConcreteStrategy 对象(通过 Context 提供的接口)。
③ 客户端向 Context 对象发送请求。
④ Context 对象将其请求转发给其持有的 Strategy 对象。
⑤ ConcreteStrategy 对象执行具体的算法。在某些情况下,策略可能需要访问 Context 的数据才能执行算法,此时 Context 会将必要的数据作为参数传递给 Strategy 的方法。
优点 (Advantages)
⚝ 相关算法族的封装 (Encapsulates Families of Algorithms): 将不同的算法封装到独立的类中,形成一个算法族,每个策略类代表一种算法。
⚝ 使得算法可以独立于其客户而变化 (Makes Algorithms Independent of their Clients): 可以在不改变 Context 类的情况下,增加、删除或修改 ConcreteStrategy 类。
⚝ 消除了条件语句 (Eliminates Conditional Statements): 将算法的选择从 Context 中移除,替换为策略对象的选择和多态调用。
⚝ 提供了一种替代继承的方案 (Provides an Alternative to Subclassing): 如果一个类通过继承来提供多种算法变体,会导致子类数量膨胀。策略模式允许通过组合来提供不同的算法。
缺点 (Disadvantages)
⚝ 增加了对象的数量 (Increased Number of Objects): 每种算法都需要一个独立的策略类,增加了类的数量。
⚝ 客户端必须知道不同的 Strategy (Client Must Know Different Strategies): 客户端需要了解不同的具体策略类,以便选择和配置 Context 使用的策略。如果策略数量很多,客户端可能会变得复杂。
⚝ 策略与 Context 之间的通信开销 (Communication Overhead between Strategy and Context): 如果策略需要频繁访问 Context 的数据,可能需要在策略接口中定义相应的参数或回调方法。
C++实现示例 (C++ Implementation Example)
模拟一个简单的排序器,支持不同的排序算法。
1
#include <iostream>
2
#include <vector>
3
#include <memory> // For std::shared_ptr
4
#include <algorithm> // For std::sort, std::reverse
5
#include <functional> // For std::function
6
7
// 抽象策略接口 (Strategy)
8
template <typename T>
9
class SortStrategy {
10
public:
11
virtual ~SortStrategy() = default;
12
// 定义排序算法接口
13
virtual void sort(std::vector<T>& data) const = 0;
14
};
15
16
// 具体策略 (ConcreteStrategy):冒泡排序
17
template <typename T>
18
class BubbleSortStrategy : public SortStrategy<T> {
19
public:
20
void sort(std::vector<T>& data) const override {
21
std::cout << "使用冒泡排序..." << std::endl;
22
int n = data.size();
23
for (int i = 0; i < n - 1; ++i) {
24
for (int j = 0; j < n - i - 1; ++j) {
25
if (data[j] > data[j + 1]) {
26
std::swap(data[j], data[j + 1]);
27
}
28
}
29
}
30
}
31
};
32
33
// 具体策略 (ConcreteStrategy):标准库排序 (高效)
34
template <typename T>
35
class StdSortStrategy : public SortStrategy<T> {
36
public:
37
void sort(std::vector<T>& data) const override {
38
std::cout << "使用 std::sort (标准库排序)..." << std::endl;
39
std::sort(data.begin(), data.end());
40
}
41
};
42
43
// Context (上下文)
44
template <typename T>
45
class Sorter {
46
private:
47
std::shared_ptr<SortStrategy<T>> m_strategy; // 当前使用的排序策略
48
49
public:
50
// 构造函数或设置方法注入策略
51
Sorter(std::shared_ptr<SortStrategy<T>> strategy) : m_strategy(strategy) {}
52
53
void setStrategy(std::shared_ptr<SortStrategy<T>> strategy) {
54
m_strategy = strategy;
55
}
56
57
// 客户端调用的方法,委托给策略对象
58
void performSort(std::vector<T>& data) const {
59
if (m_strategy) {
60
m_strategy->sort(data);
61
} else {
62
std::cout << "没有设置排序策略。" << std::endl;
63
}
64
}
65
};
66
67
// 客户端代码 (Client)
68
int main() {
69
std::vector<int> numbers = {5, 2, 8, 1, 9, 4, 6, 3, 7};
70
71
// 选择并配置具体策略
72
auto bubbleSorter = std::make_shared<BubbleSortStrategy<int>>();
73
Sorter<int> sorter(bubbleSorter); // 初始使用冒泡排序
74
75
std::vector<int> data1 = numbers;
76
sorter.performSort(data1);
77
std::cout << "排序结果:";
78
for (int x : data1) { std::cout << " " << x; }
79
std::cout << std::endl;
80
std::cout << "--------------------" << std::endl;
81
82
// 切换策略
83
auto stdSorter = std::make_shared<StdSortStrategy<int>>();
84
sorter.setStrategy(stdSorter); // 切换到标准库排序
85
86
std::vector<int> data2 = numbers;
87
sorter.performSort(data2);
88
std::cout << "排序结果:";
89
for (int x : data2) { std::cout << " " << x; }
90
std::cout << std::endl;
91
std::cout << "--------------------" << std::endl;
92
93
return 0;
94
}
在这个例子中,SortStrategy
是抽象策略接口。BubbleSortStrategy
和 StdSortStrategy
是具体的排序策略。Sorter
是 Context
,它持有一个指向 SortStrategy
的智能指针,并在 performSort
方法中委托给当前的策略对象执行排序。客户端负责创建具体的策略对象,并通过 Sorter
的构造函数或 setStrategy
方法将其设置给 Context,从而决定使用哪种排序算法。这使得客户端可以在运行时根据需要自由切换排序算法,而无需修改 Sorter
类的代码。
适用性 (Applicability)
策略模式适用于以下情况:
⚝ 定义一个算法族,并且希望这些算法可以互换。
⚝ 算法需要根据客户端的不同需求动态地变化。
⚝ 避免在算法的使用者(Context)中暴露复杂的算法细节或大量的条件判断。
⚝ 一个类定义了多种行为,并且这些行为可以通过多重的条件语句进行选择。考虑将相关的条件分支移到它们自己的 Strategy 类中。
相关模式 (Related Patterns)
⚝ 状态模式 (State Pattern): 如前所述,状态模式和策略模式结构相似,但意图不同。策略模式关注算法的可替换性,而状态模式关注对象行为随状态的变化。
⚝ 工厂方法模式 (Factory Method Pattern): Context 可以使用工厂方法来创建 Strategy 对象,从而将策略对象的创建与 Context 解耦。客户端可以通过工厂方法根据标识符获取策略,而无需直接创建具体的策略类。
⚝ 模板方法模式 (Template Method Pattern): 模板方法模式使用继承来改变算法的一部分,而策略模式使用委托来替换整个算法。策略模式通常更灵活,因为可以在运行时切换策略,而模板方法是在编译时固定的。
5.8.1 基于继承、函数对象和Lambda的实现 (Inheritance, Function Objects, and Lambda based Implementations)
在C++中实现策略模式,除了上面基于继承和多态的经典方式,还可以利用函数对象 (Function Objects) 和 Lambda 表达式 (Lambda Expressions) 这些现代C++特性,提供更灵活或更轻量级的实现。
① 基于继承和多态 (Inheritance and Polymorphism):
▮▮▮▮⚝ 这是最经典的实现方式,如上面的排序示例所示。
▮▮▮▮⚝ 使用抽象基类定义策略接口,具体策略类继承并实现接口。
▮▮▮▮⚝ Context 持有指向抽象策略基类的指针或智能指针。
▮▮▮▮⚝ 优点是结构清晰,符合面向对象原则。缺点是每种策略都需要一个单独的类,对于简单的算法可能显得繁琐。
② 基于函数对象 (Function Objects / Functors):
▮▮▮▮⚝ 函数对象是重载了函数调用运算符 ()
的类或结构体。它们可以像函数一样被调用,但可以携带状态。
▮▮▮▮⚝ 可以定义一个函数对象的概念(或者使用 std::function
作为接口),而不是一个抽象基类。
▮▮▮▮⚝ ConcreteStrategy 变成了具体的函数对象类。
▮▮▮▮⚝ Context 可以持有一个 std::function
对象来调用策略。
1
#include <iostream>
2
#include <vector>
3
#include <algorithm>
4
#include <functional> // For std::function
5
6
// 基于函数对象的策略:冒泡排序
7
struct BubbleSortFunctor {
8
template <typename T>
9
void operator()(std::vector<T>& data) const { // 重载 () 运算符
10
std::cout << "使用冒泡排序 (函数对象)..." << std::endl;
11
int n = data.size();
12
for (int i = 0; i < n - 1; ++i) {
13
for (int j = 0; j < n - i - 1; ++j) {
14
if (data[j] > data[j + 1]) {
15
std::swap(data[j], data[j + 1]);
16
}
17
}
18
}
19
}
20
};
21
22
// 基于函数对象的策略:标准库排序
23
struct StdSortFunctor {
24
template <typename T>
25
void operator()(std::vector<T>& data) const {
26
std::cout << "使用 std::sort (函数对象)..." << std::endl;
27
std::sort(data.begin(), data.end());
28
}
29
};
30
31
// Context 使用 std::function
32
template <typename T>
33
class SorterFunctor {
34
private:
35
// 使用 std::function 存储任何可调用对象
36
std::function<void(std::vector<T>&)> m_strategy;
37
38
public:
39
// 构造函数或设置方法接受任何可调用对象
40
SorterFunctor(std::function<void(std::vector<T>&)> strategy) : m_strategy(strategy) {}
41
42
void setStrategy(std::function<void(std::vector<T>&)> strategy) {
43
m_strategy = strategy;
44
}
45
46
void performSort(std::vector<T>& data) const {
47
if (m_strategy) {
48
m_strategy(data); // 直接调用 std::function 对象
49
} else {
50
std::cout << "没有设置排序策略 (函数对象)。" << std::endl;
51
}
52
}
53
};
54
55
int main() {
56
std::vector<int> numbers = {5, 2, 8, 1, 9, 4, 6, 3, 7};
57
58
// 使用函数对象配置策略
59
SorterFunctor<int> sorter(BubbleSortFunctor()); // 使用冒泡排序函数对象
60
61
std::vector<int> data1 = numbers;
62
sorter.performSort(data1);
63
std::cout << "排序结果:";
64
for (int x : data1) { std::cout << " " << x; }
65
std::cout << std::endl;
66
std::cout << "--------------------" << std::endl;
67
68
// 切换策略,使用标准库排序函数对象
69
sorter.setStrategy(StdSortFunctor());
70
71
std::vector<int> data2 = numbers;
72
sorter.performSort(data2);
73
std::cout << "排序结果:";
74
for (int x : data2) { std::cout << " " << x; }
75
std::cout << std::endl;
76
std::cout << "--------------------" << std::endl;
77
78
return 0;
79
}
这种方式避免了为策略定义抽象基类,并且函数对象可以携带状态。std::function
使得 Context 可以存储和调用任何符合签名的可调用对象(函数、函数指针、函数对象、Lambda)。
③ 基于 Lambda 表达式 (Lambda Expressions):
▮▮▮▮⚝ Lambda 表达式是 C++11 引入的匿名函数,可以方便地创建临时的函数对象。
▮▮▮▮⚝ 对于简单的策略,可以直接使用 Lambda 表达式作为策略实现,传递给 Context。
▮▮▮▮⚝ 这可以进一步简化代码,特别适合策略逻辑非常简单或者不需要复用的场景。
1
#include <iostream>
2
#include <vector>
3
#include <algorithm>
4
#include <functional> // For std::function
5
6
// Context 仍然使用 std::function
7
template <typename T>
8
class SorterLambda {
9
private:
10
std::function<void(std::vector<T>&)> m_strategy;
11
12
public:
13
SorterLambda(std::function<void(std::vector<T>&)> strategy) : m_strategy(strategy) {}
14
15
void setStrategy(std::function<void(std::vector<T>&)> strategy) {
16
m_strategy = strategy;
17
}
18
19
void performSort(std::vector<T>& data) const {
20
if (m_strategy) {
21
m_strategy(data);
22
} else {
23
std::cout << "没有设置排序策略 (Lambda)。" << std::endl;
24
}
25
}
26
};
27
28
int main() {
29
std::vector<int> numbers = {5, 2, 8, 1, 9, 4, 6, 3, 7};
30
31
// 使用 Lambda 表达式配置策略:冒泡排序
32
auto bubbleSortLambda = [](std::vector<int>& data) {
33
std::cout << "使用冒泡排序 (Lambda)..." << std::endl;
34
int n = data.size();
35
for (int i = 0; i < n - 1; ++i) {
36
for (int j = 0; j < n - i - 1; ++j) {
37
if (data[j] > data[j + 1]) {
38
std::swap(data[j], data[j + 1]);
39
}
40
}
41
}
42
};
43
44
SorterLambda<int> sorter(bubbleSortLambda);
45
46
std::vector<int> data1 = numbers;
47
sorter.performSort(data1);
48
std::cout << "排序结果:";
49
for (int x : data1) { std::cout << " " << x; }
50
std::cout << std::endl;
51
std::cout << "--------------------" << std::endl;
52
53
// 切换策略,使用标准库排序 Lambda
54
auto stdSortLambda = [](std::vector<int>& data) {
55
std::cout << "使用 std::sort (Lambda)..." << std::endl;
56
std::sort(data.begin(), data.end());
57
};
58
sorter.setStrategy(stdSortLambda);
59
60
std::vector<int> data2 = numbers;
61
sorter.performSort(data2);
62
std::cout << "排序结果:";
63
for (int x : data2) { std::cout << " " << x; }
64
std::cout << "--------------------" << std::endl;
65
66
return 0;
67
}
这种方式在现代C++中非常常见和便捷,特别适合作为回调函数或简单的策略实现。Lambda 表达式可以捕获其定义范围内的变量,使其具有函数对象的能力。
总结:C++提供了多种实现策略模式的技术。经典的基于继承和多态的方式提供了良好的结构化;基于函数对象(结合 std::function
)和 Lambda 表达式的方式则提供了更高的灵活性和简洁性,特别是在处理轻量级策略时。选择哪种实现方式取决于算法的复杂性、是否需要携带状态、以及代码的风格偏好。
5.9 模板方法模式 (Template Method Pattern)
意图 (Intent)
定义一个操作中的算法的骨架 (skeleton),而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下重新定义该算法的某些特定步骤。
动机 (Motivation)
假设您正在开发一个数据处理应用程序,需要从不同来源(例如文件、数据库、网络流)读取数据,进行一些标准处理步骤,然后输出处理结果。处理流程的总体结构是固定的:读取 -> 处理 -> 输出。然而,具体的读取、处理和输出方式可能因数据来源或目标不同而异。
如果您在基类中实现整个处理流程,那么每当需要支持新的数据源时,您可能不得不复制粘贴大量代码,或者在基类中引入条件语句来处理不同的变体,这会使得代码难以维护和扩展。
模板方法模式提供了一种解决方案:在抽象基类中定义一个“模板方法” (Template Method),这个方法包含了算法的骨架,由一系列基本操作 (Primitive Operations) 调用组成。这些基本操作可以是抽象的(必须由子类实现),也可以是具体的(在基类中提供默认实现),或者是一些钩子方法 (Hook Methods),允许子类在特定点插入自定义逻辑。具体的算法变体则通过在子类中实现(或覆盖)这些基本操作来提供。这样,算法的总体结构由基类固定,而具体实现细节则委托给子类,实现了“好莱坞原则” (Hollywood Principle):“不要调用我们,让我们来调用你” (Don't call us, we'll call you)。
结构 (Structure)
模式的关键参与者包括:
⚝ AbstractClass (抽象类):
▮▮▮▮⚝ 定义抽象的基本操作,子类必须实现这些操作。
▮▮▮▮⚝ 定义具体的基本操作或钩子操作,子类可以覆盖这些操作。
▮▮▮▮⚝ 定义一个模板方法,该方法调用基本操作,构成一个算法的骨架。模板方法通常是 final
的,以防止子类修改算法结构。
⚝ ConcreteClass (具体类):
▮▮▮▮⚝ 实现 AbstractClass 中定义的抽象基本操作。
▮▮▮▮⚝ 覆盖 AbstractClass 中定义的具体基本操作或钩子操作,以实现特定的算法变体。
一个简化的UML类图结构(文本描述):
1
graph LR
2
Client --> AbstractClass
3
AbstractClass <|-- ConcreteClassA
4
AbstractClass <|-- ConcreteClassB
5
AbstractClass --> AbstractClass: templateMethod() calls primitiveOperation()
6
ConcreteClassA --> AbstractClass: implements primitiveOperation()
7
ConcreteClassB --> AbstractClass: implements primitiveOperation()
协作 (Collaboration)
① 客户端创建 ConcreteClass 实例。
② 客户端调用 ConcreteClass 实例继承自 AbstractClass 的模板方法。
③ 模板方法在 AbstractClass 中执行,并按照预定的顺序调用基本操作。
④ 部分基本操作在 AbstractClass 中有默认实现,部分是抽象的,必须由 ConcreteClass 提供实现。
⑤ ConcreteClass 通过实现或覆盖基本操作,提供了算法中可变部分的具体行为。钩子方法允许 ConcreteClass 在模板方法的特定点插入可选行为。
优点 (Advantages)
⚝ 复用公共行为 (Code Reusability): 将算法的公共部分实现在 AbstractClass 的模板方法中,避免在多个子类中重复编写相同的代码。
⚝ 将可变部分封装在子类中 (Putting the Variable Parts in Subclasses): 算法的可变细节被推迟到 ConcreteClass 中实现,遵循了开放/封闭原则(对扩展开放,对修改封闭)。
⚝ 控制子类行为 (Controlling Subclass Behavior): 模板方法定义了算法的骨架,强制子类遵循特定的流程,限制了子类修改算法结构的能力。
⚝ “好莱坞原则”的应用 (Adherence to Hollywood Principle): 基类控制流程,调用子类的特定方法,避免了子类过度调用基类或其他兄弟类,降低了耦合。
缺点 (Disadvantages)
⚝ 父类对子类的限制 (Restrictions Imposed by the Parent Class): 算法的骨架在基类中固定,如果需要完全不同的算法结构,模板方法模式就不适用。
⚝ 增加了类的数量 (Increased Number of Classes): 每种算法变体通常需要一个新的 ConcreteClass。
⚝ 子类可能被迫实现不相关的方法 (Subclasses May Be Forced to Implement Irrelevant Methods): 如果 AbstractClass 定义了许多基本操作,即使某个 ConcreteClass 只需要其中一部分,也可能需要实现所有抽象方法(除非通过钩子方法或提供默认空实现)。
C++实现示例 (C++ Implementation Example)
模拟一个通用的构建流程,例如构建软件项目:配置 -> 编译 -> 测试 -> 部署。其中配置和部署可能因项目类型而异,编译和测试是标准步骤。
1
#include <iostream>
2
#include <string>
3
4
// 抽象类 (AbstractClass)
5
class BuildProcess {
6
public:
7
virtual ~BuildProcess() = default;
8
9
// 模板方法 (Template Method)
10
// 定义了构建算法的骨架,通常声明为 final
11
void runBuild() const {
12
configure(); // 步骤 1: 配置 (可变或钩子)
13
compile(); // 步骤 2: 编译 (公共/默认实现)
14
test(); // 步骤 3: 测试 (公共/默认实现)
15
deploy(); // 步骤 4: 部署 (可变或钩子)
16
hookAfterBuild(); // 钩子方法
17
}
18
19
protected:
20
// 抽象基本操作 (Primitive Operations) - 必须由子类实现
21
// virtual void configure() = 0; // 如果配置逻辑完全不同,可以是抽象的
22
// virtual void deploy() = 0; // 如果部署逻辑完全不同,可以是抽象的
23
24
// 具体基本操作或钩子操作 - 子类可以覆盖
25
virtual void configure() const {
26
std::cout << "执行默认配置步骤..." << std::endl;
27
}
28
29
virtual void compile() const {
30
std::cout << "执行标准编译步骤..." << std::endl;
31
}
32
33
virtual void test() const {
34
std::cout << "执行标准测试步骤..." << std::endl;
35
}
36
37
virtual void deploy() const {
38
std::cout << "执行默认部署步骤..." << std::endl;
39
}
40
41
// 钩子方法 (Hook Method) - 提供默认空实现,子类可选地覆盖以插入行为
42
virtual void hookAfterBuild() const {}
43
};
44
45
// 具体类 (ConcreteClass):构建 Web 项目
46
class WebBuild : public BuildProcess {
47
protected:
48
// 覆盖基本操作以实现 Web 项目的特定行为
49
void configure() const override {
50
std::cout << "执行 Web 项目配置步骤 (安装依赖)..." << std::endl;
51
}
52
53
// compile 和 test 使用基类默认实现
54
55
void deploy() const override {
56
std::cout << "执行 Web 项目部署步骤 (部署到 Web 服务器)..." << std::endl;
57
}
58
59
void hookAfterBuild() const override {
60
std::cout << "Web 项目构建完成,执行额外的清理工作..." << std::endl;
61
}
62
};
63
64
// 具体类 (ConcreteClass):构建桌面应用项目
65
class DesktopBuild : public BuildProcess {
66
protected:
67
// 覆盖基本操作以实现桌面应用项目的特定行为
68
void configure() const override {
69
std::cout << "执行桌面应用项目配置步骤 (检查系统库)..." << std::endl;
70
}
71
72
// compile 和 test 使用基类默认实现
73
74
void deploy() const override {
75
std::cout << "执行桌面应用项目部署步骤 (打包安装程序)..." << std::endl;
76
}
77
};
78
79
// 客户端代码 (Client)
80
int main() {
81
std::cout << "--- 构建 Web 项目 ---" << std::endl;
82
WebBuild webBuilder;
83
webBuilder.runBuild(); // 调用模板方法
84
std::cout << "--------------------" << std::endl;
85
86
std::cout << "\n--- 构建桌面应用项目 ---" << std::endl;
87
DesktopBuild desktopBuilder;
88
desktopBuilder.runBuild(); // 调用模板方法
89
std::cout << "--------------------" << std::endl;
90
91
return 0;
92
}
在这个例子中,BuildProcess
是 AbstractClass
,它定义了 runBuild
模板方法,其中包含了固定的构建流程(配置 -> 编译 -> 测试 -> 部署)。它还定义了一些基本操作:configure
和 deploy
提供默认实现但允许子类覆盖(也可以定义为纯虚函数强制子类实现),compile
和 test
提供标准实现。hookAfterBuild
是一个钩子方法,提供一个空的默认实现,子类可以选择是否覆盖以插入额外的步骤。WebBuild
和 DesktopBuild
是 ConcreteClass
,它们继承自 BuildProcess
并覆盖了 configure
和 deploy
方法,提供了特定于项目类型的实现。客户端只需要创建具体的构建器对象,并调用其 runBuild
模板方法,即可按照固定的流程执行特定于项目的构建步骤。
适用性 (Applicability)
模板方法模式适用于以下情况:
⚝ 一次性实现一个算法的固定不变的部分,并将可变的行为留给子类来实现。
⚝ 各个子类中公共的行为应该被提取出来并放到一个公共父类中,以避免代码重复。
⚝ 控制子类的扩展。模板方法在父类中定义了算法的框架,子类只能在框架内实现具体步骤。
相关模式 (Related Patterns)
⚝ 工厂方法模式 (Factory Method Pattern): 模板方法模式中的某个基本操作可以是工厂方法,用于创建算法所需的特定对象。
⚝ 策略模式 (Strategy Pattern): 模板方法模式和策略模式都可以用于封装算法。模板方法使用继承和多态来实现算法的可变部分,算法结构固定;策略模式使用委托将整个算法封装到独立的策略对象中,算法可以完全替换。
⚝ 桥接模式 (Bridge Pattern): 虽然桥接模式主要关注抽象与实现的分离,但有时候模板方法模式中的基本操作可以通过桥接模式委托给一个独立的实现层次结构。
5.10 访问者模式 (Visitor Pattern)
意图 (Intent)
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
动机 (Motivation)
假设您正在开发一个处理图形形状(如圆形、方形、三角形)的应用程序。这些形状对象构成了对象的结构。现在,您需要在这些形状上执行多种不同的操作,例如:绘制、计算面积、保存到文件(不同格式如XML、JSON)。
一种常见的做法是在每个形状类中都添加这些操作的方法:
1
class Shape {
2
// ...
3
virtual void draw() = 0;
4
virtual double calculateArea() = 0;
5
virtual void saveAsXml() = 0;
6
virtual void saveAsJson() = 0;
7
// ...
8
};
9
10
class Circle : public Shape {
11
// ... 实现 draw, calculateArea, saveAsXml, saveAsJson ...
12
};
13
14
class Square : public Shape {
15
// ... 实现 draw, calculateArea, saveAsXml, saveAsJson ...
16
};
17
// ... 其他形状类
这种方式的缺点是:
① 当需要增加新的操作时(例如,计算周长、导出为CSV),需要修改所有的形状类,违反了开放/封闭原则。
② 将不同类型的操作(如绘制、计算、序列化)混杂在同一个形状类中,导致类职责不单一。
③ 如果操作依赖于对象结构的具体类型(例如,绘制圆形需要知道其半径,绘制方形需要知道其边长),客户端需要进行类型判断或使用 dynamic_cast
,这使得客户端代码与具体形状类型耦合。
访问者模式提供了一种将操作与其作用的对象结构分离开的解决方案。它引入一个“访问者”对象 (Visitor),该对象包含了对对象结构中各个元素进行操作的逻辑。对象结构中的每个元素(Element)都有一个接受访问者的方法 (accept()
)。当一个访问者访问某个元素时,元素调用访问者的特定方法,并将自身作为参数传递给该方法。访问者模式利用了双重分派 (Double Dispatch) 的概念:操作的执行取决于两个对象的类型——元素的类型和访问者的类型。
结构 (Structure)
模式的关键参与者包括:
⚝ Visitor (访问者):
▮▮▮▮⚝ 声明一组 Visit()
操作,每个操作对应于对象结构中的一个具体的 Element 类。Visit()
操作的参数是相应的具体 Element 对象。
⚝ ConcreteVisitor (具体访问者):
▮▮▮▮⚝ 实现 Visitor 声明的接口,为每个 Element 提供具体的访问逻辑。
⚝ Element (元素):
▮▮▮▮⚝ 定义一个 accept()
操作,该操作接受一个 Visitor 对象作为参数。
⚝ ConcreteElement (具体元素):
▮▮▮▮⚝ 实现 Element 接口。在 accept(Visitor* visitor)
方法中,它调用 visitor->VisitConcreteElement(this)
。
⚝ ObjectStructure (对象结构):
▮▮▮▮⚝ 是一个包含 Element 对象的集合,例如列表、树或其他复合结构。
▮▮▮▮⚝ 提供一个遍历其元素并让访问者访问每个元素的接口(例如 accept(Visitor* visitor)
)。
一个简化的UML类图结构(文本描述):
1
graph LR
2
Client --> Visitor
3
Client --> ObjectStructure
4
Visitor <|-- ConcreteVisitorA
5
Visitor <|-- ConcreteVisitorB
6
Element <|-- ConcreteElement1
7
Element <|-- ConcreteElement2
8
ConcreteElement1 --> Visitor: accept(visitor) calls visitor->VisitConcreteElement1(this)
9
ConcreteElement2 --> Visitor: accept(visitor) calls visitor->VisitConcreteElement2(this)
10
ObjectStructure --> Element: manages/iterates
11
ObjectStructure --> Visitor: accept(visitor) calls element.accept(visitor)
协作 (Collaboration)
① 客户端创建 ConcreteVisitor 实例,并向 ObjectStructure 发送一个 accept()
请求。
② ObjectStructure 遍历其包含的 Element 集合。
③ 对于每个 Element,ObjectStructure 调用其 accept()
方法,并将 Visitor 对象作为参数传递。
④ Element 的 accept()
方法调用 Visitor 的特定 Visit()
方法,并将自身 (this
) 作为参数传递。这样,Visit()
方法的参数类型就确定了 Element 的具体类型。
⑤ ConcreteVisitor 的 Visit()
方法根据 Element 的具体类型执行相应的操作。
优点 (Advantages)
⚝ 易于增加新操作 (Ease of Adding New Operations): 增加新的操作只需创建一个新的 ConcreteVisitor 类,无需修改现有的 Element 类,符合开放/封闭原则。
⚝ 将相关的操作集中到一个访问者类中 (Gathering Related Operations): 与对象结构中不同 Element 相关的操作被集中到同一个访问者类中,方便管理和修改。
⚝ 使对象结构和操作分离 (Separating Object Structure from Operations): Element 类专注于维护其内部数据和结构,而操作逻辑则位于 Visitor 类中。
⚝ 双重分派的利用 (Leveraging Double Dispatch): 模式的核心在于操作的执行依赖于 Element 和 Visitor 的具体类型。
缺点 (Disadvantages)
⚝ 难以增加新的 Element 类型 (Difficulty of Adding New Element Types): 每当增加一个新的 ConcreteElement 类时,都需要修改所有的 Visitor 接口和 ConcreteVisitor 类,为新的元素添加对应的 Visit()
方法,这违反了开放/封闭原则。
⚝ 破坏了封装性 (Breaking Encapsulation): 为了让 Visitor 能够访问 Element 的内部状态以执行操作,Element 类通常需要暴露其内部状态,或者将 Visitor 设置为友元,这可能破坏 Element 的封装性。
⚝ Visitor 接口的复杂度 (Complexity of the Visitor Interface): Visitor 接口通常包含了对象结构中所有 Element 类型的 Visit()
方法,当 Element 数量很多时,Visitor 接口会变得非常庞大。
⚝ 遍历方式耦合 (Traversal Coupled with ObjectStructure): 访问者模式本身不包含遍历对象结构的逻辑,遍历通常由 ObjectStructure 实现。这意味着访问者依赖于 ObjectStructure 提供的遍历接口。
C++实现示例 (C++ Implementation Example)
模拟处理图形形状,并支持计算面积和导出为XML的操作。
1
#include <iostream>
2
#include <vector>
3
#include <cmath> // For M_PI (if needed, though not in this simple example)
4
#include <string>
5
#include <memory> // For std::shared_ptr
6
7
// 前向声明 Element 和 Visitor
8
class ShapeElement;
9
class ShapeVisitor;
10
11
// 抽象元素接口 (Element)
12
class ShapeElement {
13
public:
14
virtual ~ShapeElement() = default;
15
// accept 方法,接受一个 Visitor 对象作为参数
16
virtual void accept(ShapeVisitor& visitor) = 0;
17
};
18
19
// 具体元素 (ConcreteElement):圆形
20
class Circle : public ShapeElement {
21
private:
22
double m_radius;
23
public:
24
Circle(double radius) : m_radius(radius) {}
25
double getRadius() const { return m_radius; }
26
27
void accept(ShapeVisitor& visitor) override; // 在 ShapeVisitor 定义后实现
28
};
29
30
// 具体元素 (ConcreteElement):方形
31
class Square : public ShapeElement {
32
private:
33
double m_side;
34
public:
35
Square(double side) : m_side(side) {}
36
double getSide() const { return m_side; }
37
38
void accept(ShapeVisitor& visitor) override; // 在 ShapeVisitor 定义后实现
39
};
40
41
// 抽象访问者接口 (Visitor)
42
class ShapeVisitor {
43
public:
44
virtual ~ShapeVisitor() = default;
45
// 为每个具体元素类型定义 Visit 操作
46
virtual void visitCircle(Circle& circle) = 0;
47
virtual void visitSquare(Square& square) = 0;
48
// ... 其他形状的 Visit 方法
49
};
50
51
// 实现 ConcreteElement 的 accept 方法,它们需要 ShapeVisitor 的完整定义
52
void Circle::accept(ShapeVisitor& visitor) {
53
visitor.visitCircle(*this); // 双重分派:visitor.visitCircle(this 的具体类型)
54
}
55
56
void Square::accept(ShapeVisitor& visitor) {
57
visitor.visitSquare(*this); // 双重分派:visitor.visitSquare(this 的具体类型)
58
}
59
60
61
// 具体访问者 (ConcreteVisitor):面积计算器
62
class AreaCalculator : public ShapeVisitor {
63
public:
64
double totalArea = 0.0; // 访问者可以携带状态
65
66
void visitCircle(Circle& circle) override {
67
double area = 3.14159 * circle.getRadius() * circle.getRadius(); // 访问元素的内部状态
68
totalArea += area;
69
std::cout << "计算圆形面积: " << area << std::endl;
70
}
71
72
void visitSquare(Square& square) override {
73
double area = square.getSide() * square.getSide(); // 访问元素的内部状态
74
totalArea += area;
75
std::cout << "计算方形面积: " << area << std::endl;
76
}
77
};
78
79
// 具体访问者 (ConcreteVisitor):XML 导出器
80
class XmlExporter : public ShapeVisitor {
81
private:
82
std::string m_xmlOutput;
83
84
public:
85
const std::string& getXmlOutput() const { return m_xmlOutput; }
86
87
void visitCircle(Circle& circle) override {
88
m_xmlOutput += "<circle radius=\"" + std::to_string(circle.getRadius()) + "\"/>\n";
89
std::cout << "导出圆形为 XML..." << std::endl;
90
}
91
92
void visitSquare(Square& square) override {
93
m_xmlOutput += "<square side=\"" + std::to_string(square.getSide()) + "\"/>\n";
94
std::cout << "导出方形为 XML..." << std::endl;
95
}
96
};
97
98
99
// 对象结构 (ObjectStructure):图形列表
100
class Drawing {
101
private:
102
std::vector<std::shared_ptr<ShapeElement>> m_shapes;
103
104
public:
105
void addShape(std::shared_ptr<ShapeElement> shape) {
106
m_shapes.push_back(shape);
107
}
108
109
// 提供一个接口让访问者访问所有元素
110
void accept(ShapeVisitor& visitor) {
111
std::cout << "\n对象结构正在接受访问者..." << std::endl;
112
for (const auto& shape : m_shapes) {
113
shape->accept(visitor); // 将访问者传递给每个元素
114
}
115
std::cout << "对象结构访问完成。" << std::endl;
116
}
117
};
118
119
120
// 客户端代码 (Client)
121
int main() {
122
// 创建对象结构
123
Drawing drawing;
124
drawing.addShape(std::make_shared<Circle>(5.0));
125
drawing.addShape(std::make_shared<Square>(4.0));
126
drawing.addShape(std::make_shared<Circle>(2.5));
127
128
std::cout << "--- 使用面积计算器访问者 ---" << std::endl;
129
// 创建并使用面积计算器访问者
130
AreaCalculator areaVisitor;
131
drawing.accept(areaVisitor); // 遍历图形并计算面积
132
std::cout << "总面积: " << areaVisitor.totalArea << std::endl;
133
std::cout << "--------------------" << std::endl;
134
135
std::cout << "\n--- 使用 XML 导出器访问者 ---" << std::endl;
136
// 创建并使用 XML 导出器访问者
137
XmlExporter xmlVisitor;
138
drawing.accept(xmlVisitor); // 遍历图形并导出 XML
139
std::cout << "导出的 XML:\n" << xmlVisitor.getXmlOutput() << std::endl;
140
std::cout << "--------------------" << std::endl;
141
142
return 0;
143
}
在这个例子中,ShapeElement
是抽象元素接口,Circle
和 Square
是具体元素。它们都实现了 accept(ShapeVisitor&)
方法。ShapeVisitor
是抽象访问者接口,它为每个具体元素类型定义了一个 visit
方法(例如 visitCircle
)。AreaCalculator
和 XmlExporter
是具体的访问者,它们实现了 ShapeVisitor
接口,并在对应的 visit
方法中实现了针对特定形状的操作逻辑。Drawing
是对象结构,它维护一个 ShapeElement
列表,并提供一个 accept(ShapeVisitor&)
方法,该方法内部遍历列表并对每个元素调用其 accept
方法,将访问者传递进去。客户端创建 Drawing 和具体的访问者,然后调用 drawing.accept()
来执行操作。
注意双重分派的工作原理:当 drawing.accept(areaVisitor)
被调用时,Drawing
遍历其 m_shapes
列表。假设当前元素是 Circle
对象。当调用 circle->accept(areaVisitor)
时,由于 circle
是 Circle
类型的指针,虚函数调用会分派到 Circle::accept
方法。在 Circle::accept
方法中,它调用 visitor.visitCircle(*this)
。这里的关键是 visitor
是 ShapeVisitor&
引用,但实际传入的是 areaVisitor
(一个 AreaCalculator
对象)。因此,在 visitor.visitCircle(*this)
这一步,会根据 visitor
的实际类型 (AreaCalculator
) 和参数的实际类型 (Circle&
,即 *this
) 进行第二次分派,最终调用到 AreaCalculator::visitCircle(Circle&)
方法。这就是双重分派。
适用性 (Applicability)
访问者模式适用于以下情况:
⚝ 一个对象结构包含许多不同类型的对象,并且希望对这些对象执行的操作依赖于它们的具体类别。
⚝ 需要对对象结构中的元素执行许多不同的且不相关的操作,并且希望避免在元素类中污染这些操作。
⚝ 对象结构中的元素类型变化稳定,但作用于这些元素的操作经常变化。
⚝ 需要在对象结构的元素间进行协作,这些协作行为是元素类内部不该有的。
相关模式 (Related Patterns)
⚝ 组合模式 (Composite Pattern): 访问者模式常常与组合模式结合使用。组合模式用于构建对象结构(例如树形结构),而访问者模式用于在不改变节点类的情况下对结构中的所有节点执行操作。对象结构 (ObjectStructure) 常常是组合模式中的根节点或容器。
⚝ 迭代器模式 (Iterator Pattern): 遍历对象结构以让访问者访问每个元素通常需要使用迭代器。ObjectStructure 的 accept()
方法内部可能会使用迭代器来遍历其元素。
6. C++中的设计模式应用与实践
设计模式不仅仅是纸面上的理论,它们是解决实际软件开发问题的经验总结。本章将把理论与实践相结合,深入探讨如何在真实的 C++ 项目中灵活应用前几章介绍的经典设计模式,并结合现代 C++ 的语言特性,优化模式的实现方式。我们将看到模式如何组合使用,如何与 C++ 语言自身的习语(idioms)相辅相成,以及在应用模式时需要考虑的性能因素。此外,本章还将介绍如何利用设计模式来指导代码重构,并通过分析 C++ 标准库(Standard Library)中的案例,揭示模式在实际大型库中的应用。
6.1 模式的组合与协同 (Combining and Collaborating Patterns)
大多数复杂的软件系统都不是由单一设计模式构建的。相反,它们往往是多种模式巧妙组合与协同工作的成果。理解如何将不同的模式结合起来使用,是提升设计能力的关键一步。模式的组合能够帮助我们解决更大范围、更复杂的设计问题,构建出更加灵活、可扩展和易于维护的系统架构。
⚝ 为何需要模式组合?
▮▮▮▮⚝ 单一模式往往只解决特定的、局部的设计问题。一个真实的系统包含多个子系统和模块,每个部分可能需要不同的模式来优化其内部结构或与其他部分的交互。
▮▮▮▮⚝ 通过组合模式,可以从不同的抽象层面和关注点来设计系统的各个部分,形成一个内聚且松耦合的整体。
▮▮▮▮⚝ 模式组合能够创建出新的、更高层次的设计结构,有时甚至催生出新的模式或架构风格。
模式组合通常表现为一种模式在实现时依赖于另一种模式,或者几种模式共同协作来完成一个特定的功能或实现一个特定的架构。以下是一些常见的模式组合示例:
① 抽象工厂模式 (Abstract Factory Pattern) 与单例模式 (Singleton Pattern)
▮▮▮▮ⓑ 一个抽象工厂通常需要一个具体的工厂类来创建一系列相关的产品对象。如果系统中只需要一个这样的具体工厂实例(例如,用于管理全局资源的工厂),那么可以将具体工厂设计为单例。
▮▮▮▮ⓒ 组合意图:使用单例模式确保具体工厂的唯一性,使用抽象工厂模式提供一个创建产品族的接口。
1
// 抽象工厂
2
class AbstractFactory {
3
public:
4
virtual ~AbstractFactory() = default;
5
virtual ProductA* createProductA() = 0;
6
virtual ProductB* createProductB() = 0;
7
};
8
9
// 具体工厂 (Singleton)
10
class ConcreteFactorySingleton : public AbstractFactory {
11
private:
12
ConcreteFactorySingleton() {} // 私有构造函数
13
public:
14
static ConcreteFactorySingleton& getInstance() {
15
static ConcreteFactorySingleton instance; // 局部静态变量实现线程安全单例 (C++11+)
16
return instance;
17
}
18
19
ProductA* createProductA() override {
20
// 实现创建具体产品A
21
}
22
23
ProductB* createProductB() override {
24
// 实现创建具体产品B
25
}
26
// 禁用拷贝构造和赋值
27
ConcreteFactorySingleton(const ConcreteFactorySingleton&) = delete;
28
ConcreteFactorySingleton& operator=(const ConcreteFactorySingleton&) = delete;
29
};
② 组合模式 (Composite Pattern) 与迭代器模式 (Iterator Pattern)
▮▮▮▮ⓑ 组合模式用于构建对象树形结构,表示部分-整体层次。为了遍历这个树形结构中的所有元素(无论是叶子节点还是组合节点),通常会结合使用迭代器模式。
▮▮▮▮ⓒ 组合意图:组合模式构建结构,迭代器模式提供结构遍历的标准方式,隐藏了结构内部的复杂性。
③ 策略模式 (Strategy Pattern) 与工厂模式 (Factory Pattern)
▮▮▮▮ⓑ 策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。客户端通常需要根据某些条件选择合适的策略对象。
▮▮▮▮ⓒ 组合意图:可以使用工厂模式(简单工厂、工厂方法或抽象工厂)来创建和管理这些策略对象。工厂负责根据客户端提供的参数,实例化并返回正确的策略对象,将客户端从具体的策略类中解耦。
1
// 策略接口
2
class SortingStrategy {
3
public:
4
virtual ~SortingStrategy() = default;
5
virtual void sort(std::vector<int>& data) = 0;
6
};
7
8
// 具体策略A
9
class QuickSortStrategy : public SortingStrategy {
10
public:
11
void sort(std::vector<int>& data) override { /* 实现快速排序 */ }
12
};
13
14
// 具体策略B
15
class MergeSortStrategy : public SortingStrategy {
16
public:
17
void sort(std::vector<int>& data) override { /* 实现归并排序 */ }
18
};
19
20
// 策略工厂
21
class StrategyFactory {
22
public:
23
enum class Type { QuickSort, MergeSort };
24
static std::unique_ptr<SortingStrategy> createStrategy(Type type) {
25
switch (type) {
26
case Type::QuickSort: return std::make_unique<QuickSortStrategy>();
27
case Type::MergeSort: return std::make_unique<MergeSortStrategy>();
28
default: return nullptr; // 或抛出异常
29
}
30
}
31
};
32
33
// 客户端使用
34
void process(std::vector<int>& data, StrategyFactory::Type strategyType) {
35
auto strategy = StrategyFactory::createStrategy(strategyType);
36
if (strategy) {
37
strategy->sort(data);
38
}
39
}
④ 观察者模式 (Observer Pattern) 与中介者模式 (Mediator Pattern)
▮▮▮▮ⓑ 在复杂的系统中,多个观察者可能需要对同一主体 (Subject) 的变化做出反应,并且它们之间的响应可能需要协调。直接让所有观察者互相了解或与主体直接复杂交互会导致紧耦合。
▮▮▮▮ⓒ 组合意图:引入中介者模式可以管理主体和观察者之间的关系以及观察者之间的交互逻辑。中介者可以作为事件分发中心,接收来自主体的通知,并决定哪些观察者需要更新以及以何种顺序更新,甚至协调不同观察者之间的响应,降低了观察者之间的直接依赖。
模式组合的例子不胜枚举。关键在于识别问题,理解不同模式的职责和相互关系,然后像拼积木一样将它们有机地结合起来,构建出满足设计目标的结构。在实践中,我们通常从一个核心模式开始,然后根据需要引入其他模式来完善设计。
6.2 设计模式与现代C++习语 (Patterns and Modern C++ Idioms)
C++ 语言本身有一些被广泛接受的编程习惯和技术,这些被称为 C++ 习语 (C++ Idioms)。许多 C++ 习语与设计模式有着紧密的联系,它们可能是模式在 C++ 中的特定实现方式,也可能是模式赖以实现的基础。现代 C++ (Modern C++, 指 C++11/14/17/20 及更高版本) 引入了许多新特性,这些特性极大地影响了设计模式在 C++ 中的实现方式,使得某些模式的实现更加简洁、安全和高效。
⚝ 什么是 C++ 习语?
▮▮▮▮⚝ 习语是特定语言社区中形成的,用于解决常见问题的约定俗成的编码技巧、模式或习惯用法。它们通常不是正式的设计模式,但具有模式的某些特征:是经过验证的解决方案,有名称,可以描述。
本节将重点介绍几个与设计模式紧密相关的现代 C++ 习语。
6.2.1 RAII (Resource Acquisition Is Initialization)
RAII,即“资源获取即初始化”,是 C++ 中最基础也是最重要的习语之一。它的核心思想是将资源的生命周期与对象的生命周期绑定。资源在对象的构造函数中获取,在对象的析构函数中释放。当对象超出作用域时,其析构函数会被自动调用(即使发生异常),从而保证资源被及时、安全地释放,避免资源泄露。
⚝ RAII 的本质
▮▮▮▮⚝ 利用 C++ 的栈对象和其自动调用析构函数的特性,实现自动化资源管理。
▮▮▮▮⚝ 资源可以是内存、文件句柄、网络连接、锁、线程句柄等任何需要在某个时刻获取并在之后释放的东西。
⚝ RAII 与设计模式
RAII 是许多设计模式在 C++ 中可靠实现的基础,特别是那些涉及资源管理的模式。
▮▮▮▮⚝ 单例模式 (Singleton Pattern): 虽然单例模式本身是关于实例唯一性的,但如果单例需要管理资源(如全局日志文件、配置对象等),RAII 可以在单例对象的生命周期结束时(例如,程序退出时)确保资源的正确释放。更常见的是,单例的实现可以利用 RAII 的原则来实现线程安全的延迟初始化,例如 C++11 后的局部静态变量方式。
1
class Singleton {
2
private:
3
Singleton() { std::cout << "Singleton constructor\n"; }
4
~Singleton() { std::cout << "Singleton destructor\n"; } // 资源清理可能在这里
5
public:
6
static Singleton& getInstance() {
7
static Singleton instance; // C++11 guaranteed thread-safe initialization
8
return instance;
9
}
10
// ... 其他方法 ...
11
Singleton(const Singleton&) = delete;
12
Singleton& operator=(const Singleton&) = delete;
13
};
▮▮▮▮⚝ 代理模式 (Proxy Pattern): 智能指针 (Smart Pointers) 是 RAII 的典型应用,它们也是一种特殊的代理,代理了原始指针,管理其指向的资源的生命周期。
1
// std::unique_ptr 就是一个 RAII 实现的代理,管理动态分配的内存资源
2
std::unique_ptr<SomeResource> resource = std::make_unique<SomeResource>();
3
// resource 超出作用域时,SomeResource 对象自动销毁,资源释放
▮▮▮▮⚝ 装饰模式 (Decorator Pattern): 如果装饰器需要在构造时获取或在析构时释放资源(例如,一个加密装饰器需要在构造时打开密钥文件,在析构时关闭),可以利用 RAII 来管理这些资源。
▮▮▮▮⚝ 其他模式: 任何需要在对象的生命周期内管理资源的模式(如工厂创建的对象、组合对象中的组件、观察者注册与注销等)都可以通过 RAII 确保资源安全。
RAII 是编写健壮 C++ 代码的基石,它将资源管理从业务逻辑中解耦,极大地减少了资源泄露的可能性。
6.2.2 基于策略的设计 (Policy-based Design)
基于策略的设计是一种利用 C++ 模板特性来实现高度灵活和可定制组件的泛型编程习语。它与策略模式 (Strategy Pattern) 有相似之处,但通常在编译时通过模板参数选择和组合不同的“策略”类,而不是在运行时通过多态。
⚝ 核心思想
▮▮▮▮⚝ 将一个类或组件的行为分解为多个独立的方面,每个方面由一个“策略类”来封装。
▮▮▮▮⚝ 通过模板参数将这些策略类注入到主类中。
▮▮▮▮⚝ 主类通过调用其内部持有的策略对象(通常作为基类或成员变量)来执行相应的行为。
这种方式允许在编译时灵活地替换和组合不同的策略,生成具有不同行为的最终类,而且通常可以避免运行时多态带来的虚函数调用开销。
⚝ 与策略模式的对比
▮▮▮▮⚝ 策略模式 (运行时多态):
▮▮▮▮⚝ 优点:运行时动态切换策略,策略可以从文件或用户输入中决定。
▮▮▮▮⚝ 缺点:有虚函数调用开销;所有策略必须继承自同一个基类;客户端通过基类指针/引用与策略交互。
▮▮▮▮⚝ 基于策略的设计 (编译时多态):
▮▮▮▮⚝ 优点:零开销抽象(zero-overhead abstraction),行为在编译时确定;策略类之间无需继承关系;可以更容易地组合多个策略。
▮▮▮▮⚝ 缺点:行为在编译时确定,无法运行时切换;模板代码可能更复杂,编译错误信息有时不友好。
⚝ 基于策略的设计示例 (简化的排序策略)
假设我们需要一个容器,它可以使用不同的策略来排序内部元素。
1
// 策略1: 快速排序策略
2
struct QuickSortPolicy {
3
void sort(std::vector<int>& data) const {
4
std::cout << "Using Quick Sort\n";
5
// 实现快速排序... std::sort 默认就是 intro sort (快排、堆排、插入排序混合)
6
std::sort(data.begin(), data.end());
7
}
8
};
9
10
// 策略2: 冒泡排序策略 (仅为示例,效率不高)
11
struct BubbleSortPolicy {
12
void sort(std::vector<int>& data) const {
13
std::cout << "Using Bubble Sort\n";
14
// 实现冒泡排序...
15
for (size_t i = 0; i < data.size(); ++i) {
16
for (size_t j = 0; j < data.size() - 1 - i; ++j) {
17
if (data[j] > data[j+1]) {
18
std::swap(data[j], data[j+1]);
19
}
20
}
21
}
22
}
23
};
24
25
// 主类,通过模板参数接收策略
26
template<typename SortingPolicy>
27
class MyContainer {
28
private:
29
std::vector<int> data;
30
SortingPolicy policy; // 持有策略对象
31
32
public:
33
void add(int value) { data.push_back(value); }
34
35
void sort() {
36
policy.sort(data); // 调用策略对象的行为
37
}
38
39
void print() const {
40
for (int x : data) {
41
std::cout << x << " ";
42
}
43
std::cout << std::endl;
44
}
45
};
46
47
// 使用示例
48
int main() {
49
MyContainer<QuickSortPolicy> container1;
50
container1.add(3); container1.add(1); container1.add(4); container1.add(1);
51
container1.sort();
52
container1.print();
53
54
MyContainer<BubbleSortPolicy> container2;
55
container2.add(5); container2.add(2); container2.add(8);
56
container2.sort();
57
container2.print();
58
59
return 0;
60
}
在这个例子中,MyContainer
的行为(排序方式)在编译时由模板参数 SortingPolicy
决定。QuickSortPolicy
和 BubbleSortPolicy
无需继承任何共同基类。
基于策略的设计广泛应用于 C++ 模板元编程和泛型库中,如 Boost 库的某些部分。它可以实现比传统面向对象设计模式更高的灵活性和性能(在编译时确定的情况下)。```cpp
// 策略1: 快速排序策略
struct QuickSortPolicy {
void sort(std::vector
std::cout << "Using Quick Sort\n";
// 实现快速排序... std::sort 默认就是 intro sort (快排、堆排、插入排序混合)
std::sort(data.begin(), data.end());
}
};
// 策略2: 冒泡排序策略 (仅为示例,效率不高)
struct BubbleSortPolicy {
void sort(std::vector
std::cout << "Using Bubble Sort\n";
// 实现冒泡排序...
for (size_t i = 0; i < data.size(); ++i) {
for (size_t j = 0; j < data.size() - 1 - i; ++j) {
if (data[j] > data[j+1]) {
std::swap(data[j], data[j+1]);
}
}
}
}
};
// 主类,通过模板参数接收策略
template
class MyContainer {
private:
std::vector
SortingPolicy policy; // 持有策略对象
public:
void add(int value) { data.push_back(value); }
void sort() {
policy.sort(data); // 调用策略对象的行为
}
void print() const {
for (int x : data) {
std::cout << x << " ";
}
std::cout << std::endl;
}
};
// 使用示例
int main() {
MyContainer
container1.add(3); container1.add(1); container1.add(4); container1.add(1);
container1.sort();
container1.print();
MyContainer
container2.add(5); container2.add(2); container2.add(8);
container2.sort();
container2.print();
return 0;
}
1
在这个例子中,`MyContainer` 的行为(排序方式)在编译时由模板参数 `SortingPolicy` 决定。`QuickSortPolicy` 和 `BubbleSortPolicy` 无需继承任何共同基类。
2
3
基于策略的设计广泛应用于 C++ 模板元编程和泛型库中,如 Boost 库的某些部分。它可以实现比传统面向对象设计模式更高的灵活性和性能(在编译时确定的情况下)。
4
5
#### 6.2.3 其他现代 C++ 特性与设计模式
6
7
现代 C++ 引入的许多新特性也为设计模式的实现带来了便利和新的可能性:
8
9
⚝ **智能指针 (Smart Pointers - `std::unique_ptr`, `std::shared_ptr`, `std::weak_ptr`)**
10
▮▮▮▮⚝ RAII 的具体体现,简化了内存管理。
11
▮▮▮▮⚝ 在实现各种模式时,推荐使用智能指针来管理对象生命周期,避免手动 `new` 和 `delete`,减少内存泄露和悬垂指针问题。例如,在组合模式中管理子节点、在观察者模式中管理观察者列表、在工厂模式中返回新创建的对象等。
12
```cpp
13
14
// 工厂方法返回智能指针,客户端无需关心 delete
15
std::unique_ptr<Product> FactoryMethod::createProduct() {
16
return std::make_unique<ConcreteProduct>();
17
}
⚝ std::function
和 Lambda 表达式
▮▮▮▮⚝ 提供了灵活的函数对象封装能力。
▮▮▮▮⚝ 策略模式 (Strategy Pattern): 可以使用 std::function
来表示策略,用 Lambda 表达式直接定义简单的策略,避免创建单独的策略类。
1
#include <functional>
2
#include <vector>
3
#include <algorithm>
4
5
// 使用 std::function 表示排序策略
6
using SortFunction = std::function<void(std::vector<int>&)>;
7
8
class Context {
9
private:
10
std::vector<int> data;
11
SortFunction strategy;
12
public:
13
Context(SortFunction s) : strategy(s) {}
14
void setData(const std::vector<int>& d) { data = d; }
15
void executeSort() {
16
strategy(data);
17
}
18
// ... print 方法 ...
19
};
20
21
// 客户端使用 Lambda 定义策略
22
int main() {
23
Context context([](std::vector<int>& vec){
24
std::cout << "Using lambda sort\n";
25
std::sort(vec.begin(), vec.end());
26
});
27
28
std::vector<int> my_data = {9, 5, 2, 7};
29
context.setData(my_data);
30
context.executeSort();
31
// ...
32
return 0;
33
}
▮▮▮▮⚝ 观察者模式 (Observer Pattern): std::function
可以用来存储观察者的回调函数,Lambda 表达式可以方便地定义观察者的响应逻辑。
▮▮▮▮⚝ 命令模式 (Command Pattern): std::function
可以封装一个命令的执行逻辑。
⚝ 右值引用 (Rvalue References) 和移动语义 (Move Semantics)
▮▮▮▮⚝ 提高了资源管理的效率,尤其是在对象传递和返回时。
▮▮▮▮⚝ 在实现模式时,考虑使用移动语义来避免不必要的拷贝,例如在工厂模式创建和返回大型对象时,或者在组合模式中管理子节点列表时。这有助于提升性能。
⚝ 并发特性 (std::thread
, std::mutex
, std::atomic
, std::future
, std::async
)
▮▮▮▮⚝ C++11 引入了标准库级别的并发支持。
▮▮▮▮⚝ 在实现涉及并发的设计模式时(如线程安全的单例、生产者-消费者模式 - 一种行为模式的组合),应优先使用标准库提供的工具,而不是平台特定的 API,以保证代码的可移植性和正确性。
6.3 性能考虑 (Performance Considerations)
设计模式旨在提高代码的结构性、可维护性和可扩展性,但它们并非没有代价。应用设计模式往往会引入额外的抽象层、对象数量增加或使用动态绑定(虚函数),这些都可能对程序性能产生影响。作为一个负责任的开发者,在应用设计模式时,需要权衡设计上的收益与潜在的性能开销。
⚝ 设计模式对性能的潜在影响
▮▮▮▮⚝ 增加对象数量: 创建型模式(如工厂、建造者、原型)可能会导致创建比直接构造更多的临时或辅助对象。结构型模式(如装饰器、代理、适配器、享元)也可能引入额外的对象层。
▮▮▮▮⚝ 引入间接性 (Indirection):
▮▮▮▮▮▮▮▮⚝ 虚函数调用: 行为型模式(如策略、状态、模板方法、访问者)和一些结构型模式(如桥接、装饰器、代理)广泛依赖于虚函数实现运行时多态。虚函数调用比普通函数调用有轻微的额外开销(查找虚函数表),并且可能影响编译器的内联优化。在性能敏感的代码路径中,频繁的虚函数调用可能成为瓶颈。
▮▮▮▮▮▮▮▮⚝ 指针/引用追踪: 使用指针或引用来访问实际对象(如代理、装饰器、组合)也增加了访问数据的间接性,可能影响 CPU 缓存的效率。
▮▮▮▮⚝ 更复杂的控制流: 某些模式(如责任链、中介者)通过更复杂的对象间交互来解耦,这可能使得程序执行路径不那么直观,潜在地影响可预测性和优化。
▮▮▮▮⚝ 内存开销: 额外的对象会增加内存使用。如果模式导致大量细粒度对象的创建(如享元模式使用不当反而增加开销),可能会影响内存缓存效率。
⚝ 权衡与优化策略
在实际应用中,不应过度关注模式带来的微小性能损失而放弃良好的设计。通常,设计的清晰性和可维护性带来的长期收益远大于这些微小损失。然而,在性能是关键需求的场景下,可以考虑以下策略:
① 测量而不是猜测 (Measure, Don't Guess):
▮▮▮▮ⓐ 在进行任何性能优化之前,务必使用性能分析工具 (Profiler) 来确定瓶颈所在。不要凭直觉猜测哪个模式或哪部分代码是慢的。
▮▮▮▮ⓑ 只有当确定某个模式的实现确实是性能瓶颈时,才考虑对其进行优化。
② 选择合适的模式或实现方式:
▮▮▮▮ⓐ 对于追求极致性能的代码,如果可能在编译时确定行为,可以考虑使用基于模板的实现(如基于策略的设计)代替基于虚函数的模式。
▮▮▮▮ⓑ 对于单例模式,C++11 后的局部静态变量方式通常是线程安全且高效的,避免了锁的开销(在初始化之后)。
▮▮▮▮ⓒ 对于对象创建,考虑对象池 (Object Pool) 等技术来减少频繁的对象创建和销毁开销(这本身也可以看作是特定场景下的工厂或享元模式的应用)。
③ 优化模式的内部实现:
▮▮▮▮ⓐ 在不破坏模式基本结构的前提下,优化模式内部的具体算法或数据结构。
▮▮▮▮ⓑ 使用现代 C++ 特性,如移动语义,减少不必要的拷贝。
▮▮▮▮ⓒ 确保 RAII 被正确使用,避免资源泄露导致的间接性能问题。
④ 并非所有代码都需要模式:
▮▮▮▮ⓐ 设计模式应该用在它们能带来价值的地方,而不是强制应用于所有代码。简单的功能使用简单直接的实现即可。
▮▮▮▮ⓑ 避免过度设计 (Over-engineering),即引入了模式但没有解决实际问题,反而增加了复杂性和潜在性能开销。
⑤ 推迟优化:
▮▮▮▮ⓐ 优先实现清晰、正确和可维护的代码。只有在满足了这些目标并通过性能分析确定存在瓶颈后,再考虑针对性地优化。
总之,设计模式是解决设计问题的工具,性能是系统需要关注的另一个重要方面。优秀的软件设计需要在二者之间找到平衡。理解模式的实现细节及其潜在的性能影响,能够帮助我们在设计和优化过程中做出更明智的决策。
6.4 使用设计模式进行代码重构 (Refactoring with Design Patterns)
代码重构是指在不改变代码外部行为的前提下,改进代码内部结构的过程。它是提高代码质量、可读性、可维护性和可扩展性的重要手段。设计模式不仅可以用于指导新系统的设计,更是进行代码重构的强大工具。通过将现有代码中的“坏味道” (Code Smells) 转换为设计模式,可以使代码结构更加清晰和规范。
⚝ 什么是代码“坏味道”? (Code Smells)
“坏味道”是 Martin Fowler 在其著作《重构:改善既有代码设计》中提出的概念,它们是代码中可能隐藏着更深层问题的警示信号。例如:
▮▮▮▮⚝ 重复代码 (Duplicated Code): 同样的代码块在多个地方出现。
▮▮▮▮⚝ 过长函数 (Long Method): 一个函数做了太多的事情,难以理解和维护。
▮▮▮▮⚝ 过长类 (Large Class): 一个类承担了过多的职责。
▮▮▮▮⚝ 发散式变化 (Divergent Change): 修改一个类需要修改其多个不相关的方面。
▮▮▮▮⚝ 霰弹式修改 (Shotgun Surgery): 修改一个功能需要在多个类中进行小修小改。
▮▮▮▮⚝ 依恋情结 (Feature Envy): 一个函数或方法过多地使用另一个类的成员,而不是自己所在类的成员。
▮▮▮▮⚝ 临时字段 (Temporary Field): 某个字段只在特定条件下才被使用。
设计模式可以作为解决这些“坏味道”的有效手段:
① 重构复杂的条件逻辑
▮▮▮▮ⓐ 坏味道: 函数中包含大量 if-else
或 switch
语句,根据对象的类型或状态执行不同的行为。这违反了开闭原则 (Open/Closed Principle),即对扩展开放,对修改关闭。新增一种情况需要修改现有的函数。
▮▮▮▮ⓑ 应用模式:
▮▮▮▮▮▮▮▮⚝ 策略模式 (Strategy Pattern): 如果复杂逻辑是关于选择不同的算法或行为,可以将每种行为封装到一个独立的策略类中,客户端持有策略接口,通过多态调用。
▮▮▮▮▮▮▮▮⚝ 状态模式 (State Pattern): 如果复杂逻辑是根据对象内部状态的变化来切换行为,可以将每种状态及其对应的行为封装到独立的状态类中,对象持有状态接口,行为委托给当前状态对象。
1
// 重构前 (示例): 复杂的开关状态处理
2
enum class SwitchState { Off, On, Fault };
3
void handleSwitch(SwitchState state) {
4
switch (state) {
5
case SwitchState::Off:
6
// 处理关机逻辑
7
break;
8
case SwitchState::On:
9
// 处理开机逻辑
10
break;
11
case SwitchState::Fault:
12
// 处理故障逻辑
13
break;
14
}
15
}
16
17
// 重构后 (应用状态模式):
18
class ISwitchState { // State interface
19
public:
20
virtual ~ISwitchState() = default;
21
virtual void handle() = 0;
22
};
23
class OffState : public ISwitchState { /* ... */ };
24
class OnState : public ISwitchState { /* ... */ };
25
class FaultState : public ISwitchState { /* ... */ };
26
class Switch { // Context
27
std::unique_ptr<ISwitchState> currentState;
28
public:
29
void setState(std::unique_ptr<ISwitchState> state) {
30
currentState = std::move(state);
31
}
32
void requestHandle() {
33
currentState->handle();
34
}
35
};
② 重构紧耦合的对象创建
▮▮▮▮ⓐ 坏味道: 客户端代码直接使用 new
关键字创建具体对象,导致客户端与具体类紧耦合。
▮▮▮▮ⓑ 应用模式:
▮▮▮▮▮▮▮▮⚝ 工厂方法模式 (Factory Method Pattern): 将对象的创建延迟到子类,通过工厂方法接口创建对象。
▮▮▮▮▮▮▮▮⚝ 抽象工厂模式 (Abstract Factory Pattern): 创建一系列相关对象的家族,而无需指定具体类。
▮▮▮▮▮▮▮▮⚝ 简单工厂 (Simple Factory): 虽然不是 GoF 模式,但常用于集中对象创建逻辑。
这些模式可以将对象创建的职责从客户端中分离出来,提高客户端的灵活性,使其不受具体实现的约束。
③ 重构“上帝类” (God Object)
▮▮▮▮ⓐ 坏味道: 一个类承担了太多职责,包含了过多方法和数据,变得庞大而难以管理和修改。
▮▮▮▮ⓑ 应用模式:
▮▮▮▮▮▮▮▮⚝ 外观模式 (Facade Pattern): 如果“上帝类”实际上是作为多个子系统的统一接口,可以将其重构为一个外观类,其内部委托给多个更小的、职责单一的类。
▮▮▮▮▮▮▮▮⚝ 中介者模式 (Mediator Pattern): 如果“上帝类”负责协调多个对象之间的复杂交互,可以将其重构为一个中介者,将对象间的直接引用转变为通过中介者进行通信。
▮▮▮▮▮▮▮▮⚝ 策略模式/状态模式/访问者模式等: 将“上帝类”中与特定行为或状态相关的逻辑抽取出来,封装到独立的策略、状态或访问者类中。
▮▮▮▮▮▮▮▮⚝ 组合模式 (Composite Pattern): 如果“上帝类”管理着一个同构对象的集合,并且需要对集合和单个对象进行统一操作,可以考虑应用组合模式。
④ 重构对象结构
▮▮▮▮ⓐ 坏味道: 对象之间的关系僵化,难以添加新功能或修改现有结构。例如,过度使用继承导致类爆炸或层次过深;或者对象间直接引用导致紧耦合。
▮▮▮▮ⓑ 应用模式:
▮▮▮▮▮▮▮▮⚝ 桥接模式 (Bridge Pattern): 将抽象与实现分离,解决继承导致的类爆炸问题。
▮▮▮▮▮▮▮▮⚝ 适配器模式 (Adapter Pattern): 解决现有接口与所需接口不匹配的问题。
▮▮▮▮▮▮▮⑧⚝ 装饰模式 (Decorator Pattern): 以透明的方式动态地给对象添加新功能,代替继承来扩展功能。
▮▮▮▮▮▮▮⑧⚝ 代理模式 (Proxy Pattern): 为对象提供一个替代者来控制访问,可以在不修改原对象的情况下增加访问控制、延迟加载等功能。
重构的步骤:
▮▮▮▮⚝ 识别坏味道: 通过代码审查、静态分析工具或个人经验识别代码中的问题。
▮▮▮▮⚝ 确定目标模式: 根据坏味道的类型和希望达成的设计目标,选择合适的设计模式。
▮▮▮▮⚝ 小步快跑: 将重构过程分解为一系列小的、易于管理的步骤。每一步都只做一点点改动。
▮▮▮▮⚝ 频繁测试: 在重构的每个步骤后,运行单元测试和集成测试,确保没有改变代码的外部行为。
▮▮▮▮⚝ 持续改进: 重构是一个持续的过程,而不是一次性活动。
使用设计模式进行重构需要对模式有深入的理解,并结合具体的代码场景进行判断。这是一个将理论知识转化为实践经验,并不断打磨和提升代码质量的过程。
6.5 C++标准库中的设计模式案例分析 (Case Studies of Patterns in C++ Standard Library)
C++ 标准库 (Standard Library) 是一个巨大而复杂的代码库,其中蕴含了许多优秀的设计思想和模式的应用。通过分析标准库,我们可以学习到如何在实际、大规模的代码库中应用设计模式,以及现代 C++ 特性如何塑造这些模式的实现。
⚝ 标准模板库 (STL) 中的模式
STL (Standard Template Library) 是 C++ 标准库的核心组成部分,包括容器 (Containers)、迭代器 (Iterators)、算法 (Algorithms) 和函数对象 (Function Objects)。
① 迭代器模式 (Iterator Pattern)
▮▮▮▮ⓐ 应用: STL 的迭代器是迭代器模式的典型实现。它们提供了一种统一的方式来顺序访问容器中的元素,而无需暴露容器的内部结构(如数组、链表、红黑树等)。
▮▮▮▮ⓑ 结构: std::vector<T>::iterator
, std::list<T>::iterator
, std::map<Key,Value>::iterator
等都是具体迭代器,它们都遵循迭代器概念(通过操作符如 *
, ++
, ==
, !=
定义)。算法(如 std::sort
, std::find
)则是客户端,它们通过迭代器接口与各种容器协作。
▮▮▮▮ⓒ 现代 C++: 范围 for
循环 (for (auto& element : container)
) 是迭代器模式更高层次的抽象和语法糖,使得遍历更加简洁。
1
std::vector<int> v = {1, 2, 3};
2
// 使用迭代器模式遍历
3
for (auto it = v.begin(); it != v.end(); ++it) {
4
std::cout << *it << " ";
5
}
6
std::cout << std::endl;
7
// 使用范围 for 循环 (基于迭代器模式实现)
8
for (int x : v) {
9
std::cout << x << " ";
10
}
11
std::cout << std::endl;
② 策略模式 (Strategy Pattern)
▮▮▮▮ⓐ 应用: STL 算法通常接受谓词 (Predicate) 或比较函数对象作为参数,这些函数对象定义了算法行为的具体策略。例如,std::sort
可以接受一个比较函数来决定排序顺序;std::find_if
接受一个谓词来决定查找条件。
▮▮▮▮ⓑ 结构: 比较函数或谓词就是具体的策略,它们遵循特定的函数签名(策略接口),算法是客户端,它使用传递进来的策略对象来执行特定的行为。
▮▮▮ⓒ 现代 C++: Lambda 表达式使得在原地定义简单的策略变得非常方便。
1
std::vector<int> v = {3, 1, 4, 1, 5, 9};
2
// 使用 Lambda 作为策略 (降序排序)
3
std::sort(v.begin(), v.end(), [](int a, int b){
4
return a > b;
5
}); // 这是一个基于策略模式的应用
7. 案例研究:构建一个实际系统
总结: 本章通过一个或多个综合性的案例研究,演示如何在从设计到实现的整个过程中应用本书介绍的设计模式。
作为一名致力于将理论与实践相结合的讲师,我深知仅凭理论学习难以真正掌握知识的精髓。设计模式尤是如此,它们的价值必须在实际系统的构建中才能充分体现。因此,本章将带领大家深入一个具体的案例,一步步展示如何在真实的C++项目中应用前面学到的各种设计模式,并结合现代C++的特性进行优化。我们不仅会看到模式如何解决设计问题,还会讨论在选择、实现和组合模式时需要考虑的实践因素。
7.1 案例背景与需求分析 (Case Study Background and Requirements)
总结: 介绍案例的业务背景和关键功能需求。
让我们设想一个简化的多格式文档处理器 (Multi-format Document Processor) 作为我们的案例研究对象。这个应用的核心功能是处理不同格式的文档文件。
案例背景:
在许多实际场景中,软件需要与多种文件格式打交道。例如,一个文本编辑器可能需要读取和写入纯文本 (.txt
)、Markdown (.md
),甚至将来可能支持更复杂的格式如富文本 (.rtf
) 或自定义格式。如果直接在处理逻辑中硬编码对所有格式的支持,代码会变得复杂、难以维护,并且难以扩展新的格式。
核心需求分析:
我们的文档处理器需要满足以下基本需求:
① 加载文档 (Load Document):
▮▮▮▮支持从文件中读取内容。
▮▮▮▮能够根据文件扩展名或内容自动识别文件格式。
▮▮▮▮针对不同格式,需要不同的解析逻辑。
② 保存文档 (Save Document):
▮▮▮▮支持将当前文档内容保存到文件。
▮▮▮▮能够根据指定的格式或原文件格式进行保存。
▮▮▮▮针对不同格式,需要不同的序列化(或称之为写入)逻辑。
③ 文档表示 (Document Representation):
▮▮▮▮系统内部需要一个统一的方式来表示文档的内容, незалежно(独立地)于其原始文件格式。这个表示应该能够反映文档的基本结构(例如,文本块、段落等,尽管在我们的简化案例中可能只是纯文本内容)。
④ 用户操作 (User Operations):
▮▮▮▮支持基本的文档操作,例如加载、保存。
▮▮▮▮这些操作应该能够被封装和执行。
⑤ 扩展性 (Extensibility):
▮▮▮▮系统应该易于添加新的文件格式支持,而无需修改现有代码。
▮▮▮▮系统应该易于添加新的文档操作。
当前挑战与痛点:
如果不使用设计模式,一个直接的实现可能会是这样的:
⚝ 在加载和保存函数中使用大量的 if-else
或 switch
语句来判断文件格式并调用相应的处理逻辑。
⚝ 文档内容的内部表示可能与某种特定格式紧密耦合。
⚝ 用户操作逻辑直接硬编码在某个管理器类中。
这种方式会导致:
⚝ 紧耦合 (Tight Coupling): 文件格式处理逻辑与核心处理流程紧密耦合。
⚝ 低内聚 (Low Cohesion): 处理不同格式的代码分散在各处。
⚝ 难以扩展 (Hard to Extend): 添加新格式需要修改核心函数,违反了“开闭原则” (Open/Closed Principle)。
⚝ 难以维护 (Hard to Maintain): 代码庞大且逻辑交织,修改和调试困难。
基于这些需求和挑战,我们将探索如何运用设计模式来构建一个更加健壮、灵活和可扩展的文档处理器。
7.2 系统架构设计与模式选择 (System Architecture Design and Pattern Selection)
总结: 分析如何在系统的高层架构中选择和应用合适的设计模式。
为了应对上一节中提到的挑战,我们需要一个合理的系统架构来组织代码。考虑到需求中的扩展性和解耦要求,我们可以采用以下设计思路,并在此基础上引入适当的设计模式。
核心架构思路:
我们将文档处理流程分解为几个主要部分:
① 文档表示 (Document Representation): 一个独立于格式的数据结构,用于存储文档内容。
② 格式处理 (Format Handling): 负责将文件数据转换为内部文档表示(读取),以及将内部文档表示转换为文件数据(写入)。
③ 操作管理 (Operation Management): 负责封装和执行用户或系统触发的各种操作。
模式选择与应用:
基于以上思路和具体需求,我们可以考虑应用以下设计模式:
① 文件格式处理:
▮▮▮▮需求: 需要根据文件类型创建不同的读取器 (Reader) 和写入器 (Writer)。需要将读取/写入的具体算法与上层逻辑分离。
▮▮▮▮模式选择:
▮▮▮▮▮▮▮▮❶ 工厂方法模式 (Factory Method Pattern): 用于创建不同格式的读取器和写入器对象。我们可以有一个抽象的 DocumentProcessorFactory
,其子类 TextProcessorFactory
、MarkdownProcessorFactory
等负责创建各自格式对应的 DocumentReader
和 DocumentWriter
。这样,添加新格式只需添加新的工厂子类和具体产品类,符合开闭原则。
▮▮▮▮▮▮▮▮❷ 策略模式 (Strategy Pattern): 虽然工厂负责创建处理对象,但具体的读取和写入 算法 本身可以看作是独立的策略。DocumentReader
和 DocumentWriter
抽象类定义了读取和写入的接口,它们的具体子类 TextReader
、MarkdownReader
、TextWriter
、MarkdownWriter
等实现了具体的算法。上层逻辑持有接口指针,调用统一的 Read
或 Write
方法,无需关心具体实现。
② 文档表示:
▮▮▮▮需求: 需要一个灵活的方式来表示文档结构,即使在简化案例中只是纯文本,预留结构化表示的可能性也很重要。
▮▮▮▮模式选择:
▮▮▮▮▮▮▮▮❶ 组合模式 (Composite Pattern) [可选/预留]: 如果文档内容需要表示为更复杂的树状结构(例如,段落包含文本和图片),组合模式将是理想选择。对于纯文本,我们可以简化处理,但设计时应考虑未来扩展到结构化文档的可能性。在简化案例中,我们可能只用一个简单的字符串或字符串向量来表示内容,但了解组合模式在这里的潜力是重要的。
③ 用户操作管理:
▮▮▮▮需求: 封装用户操作,如加载、保存,可能还有撤销/重做(虽然撤销/重做超出了本简化案例范围,但模式应支持此可能性)。将操作的请求者与执行者解耦。
▮▮▮▮模式选择:
▮▮▮▮▮▮▮▮❶ 命令模式 (Command Pattern): 将每个操作(如加载、保存)封装成一个独立的命令对象。可以定义 Command
接口,具体命令如 LoadCommand
、SaveCommand
实现该接口。客户端创建命令对象并将其交给一个调用者 (Invoker) 执行。这使得操作可以被参数化、队列化或记录日志,并且是实现撤销/重做机制的基础。
模式间协同:
这些模式将协同工作:
⚝ 客户端或一个控制器 (Controller) 会触发一个操作,创建一个 LoadCommand
或 SaveCommand
对象(命令模式)。
⚝ LoadCommand
执行时,会利用一个机制(可能是另一个小型工厂或服务定位器)来获取正确的 DocumentProcessorFactory
,然后使用工厂方法创建特定的 DocumentReader
(工厂方法模式)。
⚝ DocumentReader
使用其内部实现的策略(策略模式)来读取文件内容,并将其转换为内部文档表示。
⚝ SaveCommand
执行时类似,使用工厂创建 DocumentWriter
,再利用写入策略将内部表示写入文件。
⚝ 如果采用了组合模式表示文档结构,读取器会将文件内容解析构建成组合结构,写入器则遍历该结构生成文件内容。
通过这样的设计,我们将系统分解为多个协作的、职责清晰的组件,每个组件都相对独立,易于理解、修改和替换。
7.3 核心模块的详细设计与实现 (Detailed Design and Implementation of Core Modules)
总结: 展示具体模块如何通过设计模式来解决特定的设计挑战。
在本节中,我们将深入具体代码层面,展示如何使用C++实现上一节中选定的设计模式。我们主要聚焦于文件格式处理(工厂方法 + 策略)和用户操作(命令模式)。
7.3.1 文件格式处理:工厂方法与策略模式的应用
首先定义处理接口:
1
#include <string>
2
#include <vector>
3
#include <memory> // for std::unique_ptr
4
5
// 内部文档表示 (Internal Document Representation) - 简化为字符串向量
6
using DocumentContent = std::vector<std::string>;
7
8
// 策略模式:文档读取器接口 (Document Reader Interface)
9
class DocumentReader {
10
public:
11
virtual ~DocumentReader() = default;
12
// 定义读取文件的纯虚函数
13
virtual DocumentContent read(const std::string& filePath) = 0;
14
};
15
16
// 策略模式:文档写入器接口 (Document Writer Interface)
17
class DocumentWriter {
18
public:
19
virtual ~DocumentWriter() = default;
20
// 定义写入文件的纯虚函数
21
virtual void write(const DocumentContent& content, const std::string& filePath) = 0;
22
};
23
24
// 工厂方法模式:抽象处理器工厂接口 (Abstract Processor Factory Interface)
25
class DocumentProcessorFactory {
26
public:
27
virtual ~DocumentProcessorFactory() = default;
28
// 工厂方法:创建读取器 (Create Reader)
29
virtual std::unique_ptr<DocumentReader> createReader() const = 0;
30
// 工厂方法:创建写入器 (Create Writer)
31
virtual std::unique_ptr<DocumentWriter> createWriter() const = 0;
32
};
接下来实现具体的策略(读取器和写入器)以及它们对应的工厂。
纯文本 (.txt) 格式的实现:
1
#include <fstream>
2
#include <iostream> // for demonstration output
3
4
// 具体策略:纯文本读取器 (Concrete Strategy: Text Reader)
5
class TextReader : public DocumentReader {
6
public:
7
DocumentContent read(const std::string& filePath) override {
8
std::ifstream file(filePath);
9
DocumentContent content;
10
if (file.is_open()) {
11
std::string line;
12
while (std::getline(file, line)) {
13
content.push_back(line);
14
}
15
file.close();
16
std::cout << "文件 '" << filePath << "' (纯文本) 读取成功。" << std::endl;
17
} else {
18
std::cerr << "无法打开文件 '" << filePath << "' 进行读取。" << std::endl;
19
}
20
return content;
21
}
22
};
23
24
// 具体策略:纯文本写入器 (Concrete Strategy: Text Writer)
25
class TextWriter : public DocumentWriter {
26
public:
27
void write(const DocumentContent& content, const std::string& filePath) override {
28
std::ofstream file(filePath);
29
if (file.is_open()) {
30
for (const auto& line : content) {
31
file << line << std::endl;
32
}
33
file.close();
34
std::cout << "文件 '" << filePath << "' (纯文本) 写入成功。" << std::endl;
35
} else {
36
std::cerr << "无法打开文件 '" << filePath << "' 进行写入。" << std::endl;
37
}
38
}
39
};
40
41
// 具体工厂:纯文本处理器工厂 (Concrete Factory: Text Processor Factory)
42
class TextProcessorFactory : public DocumentProcessorFactory {
43
public:
44
std::unique_ptr<DocumentReader> createReader() const override {
45
return std::make_unique<TextReader>();
46
}
47
48
std::unique_ptr<DocumentWriter> createWriter() const override {
49
return std::make_unique<TextWriter>();
50
}
51
};
Markdown (.md) 格式的实现 (简化版):
为了演示,我们对Markdown的解析和写入做简化处理,只作为不同格式的示例。
1
// 具体策略:Markdown 读取器 (Concrete Strategy: Markdown Reader) - 简化
2
class MarkdownReader : public DocumentReader {
3
public:
4
DocumentContent read(const std::string& filePath) override {
5
std::ifstream file(filePath);
6
DocumentContent content;
7
if (file.is_open()) {
8
std::string line;
9
while (std::getline(file, line)) {
10
// 简单的处理,例如识别标题行
11
if (line.substr(0, 1) == "#") {
12
content.push_back("标题: " + line.substr(1));
13
} else {
14
content.push_back("文本: " + line);
15
}
16
}
17
file.close();
18
std::cout << "文件 '" << filePath << "' (Markdown) 读取成功 (简化处理)。" << std::endl;
19
} else {
20
std::cerr << "无法打开文件 '" << filePath << "' 进行读取。" << std::endl;
21
}
22
return content;
23
}
24
};
25
26
// 具体策略:Markdown 写入器 (Concrete Strategy: Markdown Writer) - 简化
27
class MarkdownWriter : public DocumentWriter {
28
public:
29
void write(const DocumentContent& content, const std::string& filePath) override {
30
std::ofstream file(filePath);
31
if (file.is_open()) {
32
for (const auto& line : content) {
33
// 简单的处理,例如忽略我们读取时加的“标题: ”或“文本: ”前缀
34
if (line.substr(0, 4) == "标题: ") {
35
file << "# " << line.substr(4) << std::endl;
36
} else if (line.substr(0, 4) == "文本: ") {
37
file << line.substr(4) << std::endl;
38
} else {
39
file << line << std::endl;
40
}
41
}
42
file.close();
43
std::cout << "文件 '" << filePath << "' (Markdown) 写入成功 (简化处理)。" << std::endl;
44
} else {
45
std::cerr << "无法打开文件 '" << filePath << "' 进行写入。" << std::endl;
46
}
47
}
48
};
49
50
// 具体工厂:Markdown 处理器工厂 (Concrete Factory: Markdown Processor Factory)
51
class MarkdownProcessorFactory : public DocumentProcessorFactory {
52
public:
53
std::unique_ptr<DocumentReader> createReader() const override {
54
return std::make_unique<MarkdownReader>();
55
}
56
57
std::unique_ptr<DocumentWriter> createWriter() const override {
58
return std::make_unique<MarkdownWriter>();
59
}
60
};
工厂注册与获取机制:
为了根据文件类型动态获取正确的工厂,我们可以使用一个简单的注册表。这里可以联想到单例模式 (Singleton Pattern) 来管理这个注册表,但为了避免单例带来的全局状态问题,我们也可以选择将其作为参数传递或使用依赖注入。出于演示目的,我们使用一个简单的全局map来模拟注册。
1
#include <map>
2
3
// 简单的工厂注册表 (Simple Factory Registry)
4
class DocumentProcessorFactoryRegistry {
5
public:
6
// 使用 std::map 存储文件扩展名到工厂的映射
7
std::map<std::string, std::unique_ptr<DocumentProcessorFactory>> factories;
8
9
// 注册工厂 (Register Factory)
10
void registerFactory(const std::string& extension, std::unique_ptr<DocumentProcessorFactory> factory) {
11
factories[extension] = std::move(factory);
12
}
13
14
// 获取工厂 (Get Factory)
15
DocumentProcessorFactory* getFactory(const std::string& extension) {
16
auto it = factories.find(extension);
17
if (it != factories.end()) {
18
return it->second.get(); // 返回原始指针,所有权仍在map中
19
}
20
return nullptr; // 未找到对应的工厂
21
}
22
};
23
24
// 为了简化示例,我们创建一个全局的注册表实例 (注意:在实际应用中应避免全局变量,考虑单例或依赖注入)
25
DocumentProcessorFactoryRegistry g_factoryRegistry;
26
27
// 初始化注册表 (Initialize Registry)
28
void initializeRegistry() {
29
g_factoryRegistry.registerFactory(".txt", std::make_unique<TextProcessorFactory>());
30
g_factoryRegistry.registerFactory(".md", std::make_unique<MarkdownProcessorFactory>());
31
// ... 未来可以注册更多格式的工厂
32
}
33
34
// 获取文件扩展名 (Get File Extension) 的辅助函数
35
std::string getFileExtension(const std::string& filePath) {
36
size_t dotPos = filePath.rfind('.');
37
if (dotPos == std::string::npos) {
38
return ""; // 没有扩展名
39
}
40
return filePath.substr(dotPos);
41
}
7.3.2 用户操作:命令模式的应用
定义命令接口和具体命令:
1
// 命令模式:命令接口 (Command Interface)
2
class Command {
3
public:
4
virtual ~Command() = default;
5
virtual void execute() = 0;
6
// 理论上可以添加 undo() 和 redo() 方法支持撤销重做
7
};
8
9
// 需要一个接收者 (Receiver),例如 DocumentManager 类
10
// 假设 DocumentManager 负责持有文档内容并协调处理器的使用
11
class DocumentManager {
12
private:
13
DocumentContent currentContent;
14
// 可能会持有当前处理器的引用或智能指针,或者在Command中创建
15
// ...
16
DocumentProcessorFactoryRegistry& registry; // 引用注册表
17
18
public:
19
DocumentManager(DocumentProcessorFactoryRegistry& reg) : registry(reg) {}
20
21
// 接收者操作:加载文件 (Load File)
22
void loadFile(const std::string& filePath) {
23
std::string extension = getFileExtension(filePath);
24
DocumentProcessorFactory* factory = registry.getFactory(extension);```cpp
25
#include <string>
26
#include <vector>
27
#include <memory> // for std::unique_ptr
28
#include <fstream>
29
#include <iostream> // for demonstration output
30
#include <map>
31
#include <stdexcept> // for std::runtime_error
32
33
// 内部文档表示 (Internal Document Representation) - 简化为字符串向量
34
using DocumentContent = std::vector<std::string>;
35
36
// 策略模式:文档读取器接口 (Document Reader Interface)
37
class DocumentReader {
38
public:
39
virtual ~DocumentReader() = default;
40
// 定义读取文件的纯虚函数
41
virtual DocumentContent read(const std::string& filePath) = 0;
42
};
43
44
// 策略模式:文档写入器接口 (Document Writer Interface)
45
class DocumentWriter {
46
public:
47
virtual ~DocumentWriter() = default;
48
// 定义写入文件的纯虚函数
49
virtual void write(const DocumentContent& content, const std::string& filePath) = 0;
50
};
51
52
// 工厂方法模式:抽象处理器工厂接口 (Abstract Processor Factory Interface)
53
class DocumentProcessorFactory {
54
public:
55
virtual ~DocumentProcessorFactory() = default;
56
// 工厂方法:创建读取器 (Create Reader)
57
virtual std::unique_ptr<DocumentReader> createReader() const = 0;
58
// 工厂方法:创建写入器 (Create Writer)
59
virtual std::unique_ptr<DocumentWriter> createWriter() const = 0;
60
};
61
62
// --- 具体策略实现 ---
63
64
// 具体策略:纯文本读取器 (Concrete Strategy: Text Reader)
65
class TextReader : public DocumentReader {
66
public:
67
DocumentContent read(const std::string& filePath) override {
68
std::ifstream file(filePath);
69
DocumentContent content;
70
if (file.is_open()) {
71
std::string line;
72
while (std::getline(file, line)) {
73
content.push_back(line);
74
}
75
file.close();
76
std::cout << "文件 '" << filePath << "' (纯文本) 读取成功。" << std::endl;
77
} else {
78
std::cerr << "错误:无法打开文件 '" << filePath << "' 进行读取。" << std::endl;
79
throw std::runtime_error("无法打开文件进行读取");
80
}
81
return content;
82
}
83
};
84
85
// 具体策略:纯文本写入器 (Concrete Strategy: Text Writer)
86
class TextWriter : public DocumentWriter {
87
public:
88
void write(const DocumentContent& content, const std::string& filePath) override {
89
std::ofstream file(filePath);
90
if (file.is_open()) {
91
for (const auto& line : content) {
92
file << line << std::endl;
93
}
94
file.close();
95
std::cout << "文件 '" << filePath << "' (纯文本) 写入成功。" << std::endl;
96
} else {
97
std::cerr << "错误:无法打开文件 '" << filePath << "' 进行写入。" << std::endl;
98
throw std::runtime_error("无法打开文件进行写入");
99
}
100
}
101
};
102
103
// 具体策略:Markdown 读取器 (Concrete Strategy: Markdown Reader) - 简化
104
class MarkdownReader : public DocumentReader {
105
public:
106
DocumentContent read(const std::string& filePath) override {
107
std::ifstream file(filePath);
108
DocumentContent content;
109
if (file.is_open()) {
110
std::string line;
111
while (std::getline(file, line)) {
112
// 简单的处理,例如识别标题行
113
if (!line.empty() && line[0] == '#') {
114
content.push_back("标题: " + line.substr(1));
115
} else {
116
content.push_back("文本: " + line);
117
}
118
}
119
file.close();
120
std::cout << "文件 '" << filePath << "' (Markdown) 读取成功 (简化处理)。" << std::endl;
121
} else {
122
std::cerr << "错误:无法打开文件 '" << filePath << "' 进行读取。" << std::endl;
123
throw std::runtime_error("无法打开文件进行读取");
124
}
125
return content;
126
}
127
};
128
129
// 具体策略:Markdown 写入器 (Concrete Strategy: Markdown Writer) - 简化
130
class MarkdownWriter : public DocumentWriter {
131
public:
132
void write(const DocumentContent& content, const std::string& filePath) override {
133
std::ofstream file(filePath);
134
if (file.is_open()) {
135
for (const auto& line : content) {
136
// 简单的处理,例如忽略我们读取时加的“标题: ”或“文本: ”前缀
137
if (line.substr(0, 4) == "标题: ") {
138
file << "# " << line.substr(4) << std::endl;
139
} else if (line.substr(0, 4) == "文本: ") {
140
file << line.substr(4) << std::endl;
141
} else {
142
file << line << std::endl;
143
}
144
}
145
file.close();
146
std::cout << "文件 '" << filePath << "' (Markdown) 写入成功 (简化处理)。" << std::endl;
147
} else {
148
std::cerr << "错误:无法打开文件 '" << filePath << "' 进行写入。" << std::endl;
149
throw std::runtime_error("无法打开文件进行写入");
150
}
151
}
152
};
153
154
// --- 具体工厂实现 ---
155
156
// 具体工厂:纯文本处理器工厂 (Concrete Factory: Text Processor Factory)
157
class TextProcessorFactory : public DocumentProcessorFactory {
158
public:
159
std::unique_ptr<DocumentReader> createReader() const override {
160
return std::make_unique<TextReader>();
161
}
162
163
std::unique_ptr<DocumentWriter> createWriter() const override {
164
return std::make_unique<TextWriter>();
165
}
166
};
167
168
// 具体工厂:Markdown 处理器工厂 (Concrete Factory: Markdown Processor Factory)
169
class MarkdownProcessorFactory : public DocumentProcessorFactory {
170
public:
171
std::unique_ptr<DocumentReader> createReader() const override {
172
return std::make_unique<MarkdownReader>();
173
}
174
175
std::unique_ptr<DocumentWriter> createWriter() const override {
176
return std::make_unique<MarkdownWriter>();
177
}
178
};
179
180
// --- 工厂注册与获取机制 ---
181
182
// 获取文件扩展名 (Get File Extension) 的辅助函数
183
std::string getFileExtension(const std::string& filePath) {
184
size_t dotPos = filePath.rfind('.');
185
if (dotPos == std::string::npos) {
186
return ""; // 没有扩展名
187
}
188
return filePath.substr(dotPos);
189
}
190
191
// 简单的工厂注册表 (Simple Factory Registry)
192
class DocumentProcessorFactoryRegistry {
193
private:
194
// 使用 std::map 存储文件扩展名到工厂的映射
195
std::map<std::string, std::unique_ptr<DocumentProcessorFactory>> factories;
196
197
public:
198
// 注册工厂 (Register Factory)
199
void registerFactory(const std::string& extension, std::unique_ptr<DocumentProcessorFactory> factory) {
200
factories[extension] = std::move(factory);
201
}
202
203
// 获取工厂 (Get Factory)
204
DocumentProcessorFactory* getFactory(const std::string& extension) const {
205
auto it = factories.find(extension);
206
if (it != factories.end()) {
207
return it->second.get(); // 返回原始指针,所有权仍在map中
208
}
209
return nullptr; // 未找到对应的工厂
210
}
211
};
212
213
// 为了简化示例,我们创建一个注册表实例 (在实际应用中应考虑生命周期管理,如使用智能指针或单例)
214
DocumentProcessorFactoryRegistry g_factoryRegistry;
215
216
// 初始化注册表 (Initialize Registry) 函数,可以在程序启动时调用一次
217
void initializeRegistry() {
218
g_factoryRegistry.registerFactory(".txt", std::make_unique<TextProcessorFactory>());
219
g_factoryRegistry.registerFactory(".md", std::make_unique<MarkdownProcessorFactory>());
220
// ... 未来可以注册更多格式的工厂
221
std::cout << "文档处理器注册表初始化完成。" << std::endl;
222
}
223
224
// --- 命令模式实现 ---
225
226
// 命令模式:命令接口 (Command Interface)
227
class Command {
228
public:
229
virtual ~Command() = default;
230
virtual void execute() = 0;
231
// 理论上可以添加 undo() 和 redo() 方法支持撤销重做
232
};
233
234
// 需要一个接收者 (Receiver),例如 DocumentManager 类
235
// 假设 DocumentManager 负责持有文档内容并协调处理器的使用
236
class DocumentManager {
237
private:
238
DocumentContent currentContent;
239
// 可能会持有当前处理器的引用或智能指针,或者在Command中创建
240
// ...
241
const DocumentProcessorFactoryRegistry& registry; // 引用注册表
242
243
public:
244
// 构造函数接收注册表的引用
245
DocumentManager(const DocumentProcessorFactoryRegistry& reg) : registry(reg) {}
246
247
// 接收者操作:加载文件 (Load File)
248
void loadFile(const std::string& filePath) {
249
std::string extension = getFileExtension(filePath);
250
const DocumentProcessorFactory* factory = registry.getFactory(extension);
251
252
if (!factory) {
253
std::cerr << "错误:不支持的文件格式 '" << extension << "'" << std::endl;
254
return; // 或者抛出异常
255
}
256
257
std::unique_ptr<DocumentReader> reader = factory->createReader();
258
try {
259
currentContent = reader->read(filePath);
260
std::cout << "内容已加载到 DocumentManager." << std::endl;
261
// 可以在这里通知观察者 (如果使用了观察者模式)
262
} catch (const std::runtime_error& e) {
263
std::cerr << "加载文件失败: " << e.what() << std::endl;
264
// 清空当前内容或保持原样,取决于错误处理策略
265
}
266
}
267
268
// 接收者操作:保存文件 (Save File)
269
void saveFile(const std::string& filePath) {
270
std::string extension = getFileExtension(filePath);
271
const DocumentProcessorFactory* factory = registry.getFactory(extension);
272
273
if (!factory) {
274
std::cerr << "错误:不支持的文件格式 '" << extension << "'" << std::endl;
275
return; // 或者抛出异常
276
}
277
278
std::unique_ptr<DocumentWriter> writer = factory->createWriter();
279
try {
280
writer->write(currentContent, filePath);
281
} catch (const std::runtime_error& e) {
282
std::cerr << "保存文件失败: " << e.what() << std::endl;
283
}
284
}
285
286
// 接收者操作:获取当前文档内容 (Get Current Document Content)
287
const DocumentContent& getContent() const {
288
return currentContent;
289
}
290
291
// 接收者操作:设置当前文档内容 (Set Current Document Content)
292
void setContent(const DocumentContent& content) {
293
currentContent = content;
294
std::cout << "DocumentManager 内容已更新。" << std::endl;
295
// 可以在这里通知观察者
296
}
297
298
// 接收者操作:清空文档内容 (Clear Document Content)
299
void clearContent() {
300
currentContent.clear();
301
std::cout << "DocumentManager 内容已清空。" << std::endl;
302
// 可以在这里通知观察者
303
}
304
};
305
306
// 具体命令:加载命令 (Load Command)
307
class LoadCommand : public Command {
308
private:
309
DocumentManager* receiver;
310
std::string filePath;
311
312
public:
313
LoadCommand(DocumentManager* recv, const std::string& path) : receiver(recv), filePath(path) {}
314
315
void execute() override {
316
if (receiver) {
317
receiver->loadFile(filePath);
318
} else {
319
std::cerr << "错误:LoadCommand 的接收者 (DocumentManager) 未设置。" << std::endl;
320
}
321
}
322
};
323
324
// 具体命令:保存命令 (Save Command)
325
class SaveCommand : public Command {
326
private:
327
DocumentManager* receiver;
328
std::string filePath;
329
330
public:
331
SaveCommand(DocumentManager* recv, const std::string& path) : receiver(recv), filePath(path) {}
332
333
void execute() override {
334
if (receiver) {
335
receiver->saveFile(filePath);
336
} else {
337
std::cerr << "错误:SaveCommand 的接收者 (DocumentManager) 未设置。" << std::endl;
338
}
339
}
340
};
341
342
// 具体命令:清空文档内容命令 (Clear Content Command)
343
class ClearContentCommand : public Command {
344
private:
345
DocumentManager* receiver;
346
347
public:
348
ClearContentCommand(DocumentManager* recv) : receiver(recv) {}
349
350
void execute() override {
351
if (receiver) {
352
receiver->clearContent();
353
} else {
354
std::cerr << "错误:ClearContentCommand 的接收者 (DocumentManager) 未设置。" << std::endl;
355
}
356
}
357
};
358
359
360
// 调用者 (Invoker) - 模拟用户界面或脚本
361
class CommandInvoker {
362
private:
363
std::unique_ptr<Command> command;
364
365
public:
366
void setCommand(std::unique_ptr<Command> cmd) {
367
command = std::move(cmd);
368
}
369
370
void executeCommand() {
371
if (command) {
372
command->execute();
373
// 如果支持撤销,可以在这里将命令推入历史堆栈
374
} else {
375
std::cout << "没有设置要执行的命令。" << std::endl;
376
}
377
}
378
// 注意:这里没有实现撤销/重做功能,但命令模式为此提供了基础
379
};
组合模式 (Composite Pattern) 的潜在应用 (概念性):
虽然在简化案例中我们使用了 std::vector<std::string>
作为文档内容,但如果需要支持更复杂的文档结构,例如包含标题、段落、列表、图片等不同类型的元素,并且这些元素可以嵌套,那么组合模式将非常适合。
⚝ 定义一个抽象的 DocumentComponent
类,包含 Display()
, Add(component)
, Remove(component)
, GetChild(index)
等接口。
⚝ 定义具体组件类,如 TextElement
(叶节点 Leaf)、Paragraph
(组合节点 Composite)、Heading
(组合节点 Composite) 等,它们继承自 DocumentComponent
。
⚝ Paragraph
和 Heading
等组合节点内部持有一个 std::vector<std::unique_ptr<DocumentComponent>>
来存储子组件。
⚝ DocumentManager
将持有一个 std::unique_ptr<DocumentComponent>
作为文档的根节点。
读取器会解析文件内容并构建这个组件树,写入器则遍历这个树生成文件。例如,MarkdownReader 会解析 Markdown 语法,遇到 #
创建 Heading
对象并添加文本子节点,遇到普通文本创建 Paragraph
对象并添加 TextElement
子节点。
1
// 组合模式 (Composite Pattern) - 概念性示例
2
/*
3
// 抽象组件 (Abstract Component)
4
class DocumentComponent {
5
public:
6
virtual ~DocumentComponent() = default;
7
virtual void display() const = 0; // 例如,用于渲染或调试输出
8
// 针对组合节点的接口 (Composite-specific operations) - 默认实现抛异常或不做任何事
9
virtual void add(std::unique_ptr<DocumentComponent> component) { (void)component; throw std::runtime_error("Operation not supported"); }
10
virtual void remove(const DocumentComponent* component) { (void)component; throw std::runtime_error("Operation not supported"); }
11
virtual DocumentComponent* getChild(size_t index) const { (void)index; throw std::runtime_error("Operation not supported"); return nullptr; }
12
};
13
14
// 叶节点 (Leaf)
15
class TextElement : public DocumentComponent {
16
private:
17
std::string text;
18
public:
19
TextElement(const std::string& t) : text(t) {}
20
void display() const override { std::cout << text; }
21
};
22
23
// 组合节点 (Composite)
24
class Paragraph : public DocumentComponent {
25
private:
26
std::vector<std::unique_ptr<DocumentComponent>> children;
27
public:
28
void display() const override {
29
for (const auto& child : children) {
30
child->display();
31
}
32
std::cout << std::endl; // 段落结束换行
33
}
34
void add(std::unique_ptr<DocumentComponent> component) override {
35
children.push_back(std::move(component));
36
}
37
// ... 实现 remove 和 getChild
38
};
39
40
// 组合节点 (Composite)
41
class Heading : public DocumentComponent {
42
private:
43
std::string level; // 例如 "H1", "H2"
44
std::vector<std::unique_ptr<DocumentComponent>> children;
45
public:
46
Heading(const std::string& lvl) : level(lvl) {}
47
void display() const override {
48
std::cout << level << ": ";
49
for (const auto& child : children) {
50
child->display();
51
}
52
std::cout << std::endl;
53
}
54
void add(std::unique_ptr<DocumentComponent> component) override {
55
children.push_back(std::move(component));
56
}
57
// ... 实现 remove 和 getChild
58
};
59
60
// DocumentManager 将持有 std::unique_ptr<DocumentComponent> root;
61
// Reader 将解析文件并构建 DocumentComponent 树
62
// Writer 将遍历 DocumentComponent 树并写入文件
63
*/
在当前的简化案例中,我们不实现完整的组合模式,但理解其在表示结构化文档中的作用非常重要。我们将继续使用 DocumentContent
(即 std::vector<std::string>
) 来简化,但请记住,一个更完善的系统会采用类似组合模式的结构。
通过上面的实现,我们看到:
⚝ 文件格式处理被解耦到具体的 Reader/Writer 类中(策略模式)。
⚝ 创建 Reader/Writer 的过程被抽象到工厂方法中,使得添加新格式无需修改客户端代码(工厂方法模式)。
⚝ 用户操作被封装为独立的命令对象,使得请求者 (Invoker) 和执行者 (Receiver) 解耦(命令模式)。
这些模式的应用显著提高了系统的模块化程度、可维护性和扩展性。
7.4 测试与优化 (Testing and Optimization)
总结: 讨论如何测试应用了设计模式的代码以及如何进行性能优化。
应用了设计模式的代码通常更易于测试和优化,因为它们通常将不同的职责分离到独立的类中。
7.4.1 测试
① 单元测试 (Unit Testing):
▮▮▮▮具体策略类 (Concrete Strategy Classes - Reader/Writer): TextReader
和 MarkdownReader
等类可以独立地进行单元测试。我们可以创建模拟的文件(例如,使用字符串流 std::stringstream
或创建临时文件),然后调用 read
方法,验证返回的 DocumentContent
是否符合预期。类似地,测试 TextWriter
和 MarkdownWriter
时,可以调用 write
方法,然后读取生成的文件内容,验证其正确性。
▮▮▮▮具体工厂类 (Concrete Factory Classes): TextProcessorFactory
等类的测试相对简单,只需验证它们的 createReader
和 createWriter
方法是否返回了正确类型的对象(例如,返回的是 std::unique_ptr<TextReader>
和 std::unique_ptr<TextWriter>
)。
▮▮▮▮具体命令类 (Concrete Command Classes): LoadCommand
, SaveCommand
等可以测试它们是否正确地调用了接收者 (DocumentManager
) 的相应方法。这通常需要使用模拟对象 (Mock Objects) 来替代真实的 DocumentManager
。例如,创建一个 MockDocumentManager
类,它记录被调用的方法及其参数,然后在测试中验证 LoadCommand::execute()
是否调用了 mockManager.loadFile(filePath)
。
▮▮▮▮接收者类 (Receiver Class - DocumentManager): DocumentManager
的方法(loadFile
, saveFile
等)可以独立测试其内部逻辑。在测试 loadFile
和 saveFile
时,由于它们依赖于 DocumentProcessorFactoryRegistry
和具体的 Reader/Writer,我们可以:
▮▮▮▮▮▮▮▮❶ 向注册表注册用于测试的桩 (Stub) 或模拟工厂,它们创建的 Reader/Writer 对象行为可控,不进行实际文件I/O。
▮▮▮▮▮▮▮▮❷ 或者,如果 DocumentManager
的构造函数接受 Reader/Writer 对象的参数而不是工厂注册表(这是一种不同的设计方式,有时更便于测试),我们可以直接传递测试用的 Reader/Writer 对象。
② 集成测试 (Integration Testing):
▮▮▮▮测试命令调用者 (CommandInvoker
) 与具体命令和接收者 (DocumentManager
) 的协同工作。
▮▮▮▮测试整个流程,例如:注册工厂 -> 初始化 DocumentManager
-> 创建 LoadCommand
-> 设置并执行命令 -> 验证 DocumentManager
的内容 -> 创建 SaveCommand
-> 设置并执行命令 -> 验证生成的文件内容。
设计模式带来的好处是,各个组件之间的依赖通常是通过接口进行的,这天然地支持了使用模拟对象或桩进行测试,极大地提高了测试的便利性和可靠性。
7.4.2 性能考虑
设计模式在带来灵活性和可维护性的同时,有时也会引入一定的性能开销。在C++中,主要的开销可能来源于:
⚝ 虚函数调用 (Virtual Function Calls): 多态是许多模式(如策略、工厂方法、命令)的基础,虚函数调用比普通函数调用略慢。在大多数应用中,这种开销微不足道的,但在性能极端敏感的紧密循环中可能需要考虑。
⚝ 对象创建开销 (Object Creation Overhead): 工厂模式会频繁创建对象。现代C++的智能指针 (std::make_unique
, std::make_shared
) 优化了对象创建,但对象的构造和析构本身仍有成本。原型模式(如果使用)涉及对象拷贝,也可能带来开销。
⚝ 内存分配 (Memory Allocation): 使用 new
或 make_unique
/make_shared
涉及堆内存分配,比栈内存分配慢。如果模式导致大量短生命周期对象的创建,可能需要注意。
⚝ 间接性 (Indirection): 模式往往引入额外的层级和间接性(例如,通过指针或引用调用方法),这可能影响 CPU 缓存的效率。
优化策略:
⚝ 衡量与分析 (Measure and Profile): 在进行任何优化之前,首先使用性能分析工具 (Profiler) 确定瓶颈所在。不要过早优化。
⚝ 权衡模式与性能 (Balance Patterns and Performance): 如果某个模式在应用的性能关键路径上引入了不可接受的开销,可能需要考虑牺牲一部分模式带来的灵活性,采用更直接或针对性的实现。
⚝ 使用现代C++特性 (Use Modern C++ Features): 智能指针、右值引用/移动语义 (Move Semantics) 可以提高资源管理的效率。std::unique_ptr
通常是首选,因为它开销小且表达独占所有权。
⚝ 对象池 (Object Pooling): 对于频繁创建和销毁的细粒度对象(例如在享元模式中),可以使用对象池来减少内存分配和释放的开销。
⚝ 基于策略的模板编程 (Policy-based Design/Template Metaprogramming): 对于某些模式(如策略模式),可以使用模板在编译期绑定策略,避免运行时虚函数调用开销。这牺牲了运行时多态带来的灵活性,但提供了更高的性能。例如,可以使用 CRTP (Curiously Recurring Template Pattern) 实现静态多态。
⚝ 小对象优化 (Small Object Optimization): 对于一些小尺寸的策略或命令对象,可以考虑在 Invoker 或 Manager 内部使用 std::aligned_storage
或类似的技巧来避免堆分配,直接在栈或父对象内部存储。
在我们的文档处理器案例中,文件读取和写入操作通常是 I/O 密集型,其性能瓶颈更可能在于磁盘访问或网络传输,而不是模式引入的少量虚函数调用或对象创建。因此,通常不需要对模式实现本身进行过度优化。只有在确定是模式实现导致了性能问题时,才应该考虑上述优化手段。
7.5 从案例中学习 (Lessons Learned from the Case Study)
总结: 总结案例研究中获得的经验和教训。
通过构建这个简化的多格式文档处理器,我们学习到了如何在实际项目应用中运用设计模式,并从中获取了宝贵的经验。
① 模式是解决问题的工具,而非目的本身 (Patterns are Tools, Not Goals):
⚝ 我们不是为了使用设计模式而使用模式。我们在分析了需求和面临的挑战(特别是关于扩展性和可维护性)后,才确定哪些模式能够有效地解决这些问题。
⚝ 在案例中,针对文件格式扩展性问题,我们选择了工厂方法和策略模式;针对用户操作的封装和解耦,我们选择了命令模式。每种模式都对应着一个或一组特定的设计问题。
② 理解模式的意图和适用性至关重要 (Understanding Pattern Intent and Applicability is Crucial):
⚝ 仅仅知道模式的结构(类图)是不够的。深入理解模式旨在解决的问题 (Problem)、它在什么情境下 (Context) 适用、以及它带来的解决方案 (Solution) 和后果 (Consequences)(包括优点和缺点)才能正确地选择和应用模式。
⚝ 例如,我们认识到组合模式适用于表示树状结构,尽管在简化案例中未使用,但也指出了其在未来扩展中的潜在价值。
③ 模式可以组合使用 (Patterns Can Be Combined):
⚝ 实际系统中的复杂问题往往需要多个模式协同工作。在我们的案例中,工厂方法模式负责创建正确的策略对象,而策略模式定义了对象的行为接口和具体实现。命令模式则将这些操作封装起来,与请求者解耦。
⚝ 学习如何识别不同模式之间的潜在协同作用,是将它们应用于大型复杂系统的关键。
④ 现代C++特性影响模式的实现 (Modern C++ Features Influence Pattern Implementation):
⚝ C++11 及后续标准引入的智能指针极大地改善了资源管理,使得模式实现中的内存管理更加安全和简洁,例如在工厂方法中返回 std::unique_ptr
。
⚝ std::function
和 Lambda 表达式为策略模式和命令模式提供了更灵活的实现方式,有时可以替代传统的类继承。
⚝ 理解这些现代特性如何与经典模式相结合,是编写高质量现代C++代码的必由之路。
⑤ 设计是一个迭代过程 (Design is an Iterative Process):
⚝ 初步的设计和模式选择可能不是最优的。随着对需求理解的深入和实现的推进,可能会发现需要调整模式的应用方式,或者引入新的模式。
⚝ 例如,如果我们发现文件格式的解析和写入逻辑变得非常复杂,可能需要进一步将其分解,或者引入解释器模式来处理自定义格式。
⚝ 拥抱变化,并愿意在必要时重构代码以改进设计,是专业开发者的重要素质。设计模式提供了进行有效重构的“词汇表”和“蓝图”。
⑥ 不要过度设计 (Avoid Over-Engineering):
⚝ 不是所有问题都需要设计模式。对于简单、稳定且未来变化可能性低的功能,直接的实现可能更高效且易于理解。
⚝ 在案例中,我们对Markdown处理进行了简化,也没有实现撤销/重做,这是基于简化案例的范围考虑。在实际项目中,需要根据真实的复杂度和变化预测来决定模式的应用程度。过度使用模式会增加不必要的复杂性。
通过这个案例研究,我们不仅实践了设计模式的应用,更重要的是体会到了它们在构建灵活、可维护和可扩展的软件系统中的强大力量。将理论知识应用于实践,并在实践中反思和学习,是掌握设计模式的最终路径。我鼓励读者在自己的项目中尝试应用这些模式,从实际经验中加深理解。
8. 进阶话题与未来展望
本章将带领读者超越经典的设计模式本身,探讨软件设计领域的进阶话题。我们将审视常见的反模式,学习如何识别和避免它们;了解如何在现有系统中发现潜在的设计模式;区分和联系设计模式与更宏观的软件架构模式;并展望未来C++语言的发展如何影响设计模式的应用和表达。本章旨在拓宽读者的视野,鼓励持续学习和实践,为成为更优秀的软件设计师奠定基础。
8.1 反模式 (Anti-Patterns)
并非所有的“模式”都是好的实践。反模式(Anti-Patterns)描述了在软件开发中经常出现但低效、甚至有害的解决方案。理解反模式与理解设计模式同样重要,因为它能帮助我们识别和避免常见的陷阱,从而写出更健壮、更易维护的代码。
反模式通常是由于经验不足、缺乏远见或对问题理解不充分而导致的。它们往往在短期内看似解决了问题,但在长期维护和扩展时会带来严重的困境。
本节将介绍一些C++开发中常见的反模式,并讨论如何识别、分析和重构它们。
8.1.1 常见的C++反模式 (Common C++ Anti-Patterns)
软件开发中有许多知名的反模式,其中一些在C++环境中尤为常见或有其特殊的表现形式。
① 大泥球 (Big Ball of Mud):
▮▮▮▮这是最普遍的一种反模式,指的是一个缺乏明显结构、混乱不堪、难以理解和维护的系统。代码模块之间高度耦合,没有清晰的边界和分层。在C++中,这可能表现为:
▮▮▮▮⚝ 全局变量(Global Variables)的滥用:导致状态难以追踪和管理。
▮▮▮▮⚝ 超大的类或函数(God Class/Function):单个类承担了过多的职责,或者一个函数包含了过多的逻辑。
▮▮▮▮⚝ 复杂的依赖关系:模块之间形成错综复杂的依赖网络。
▮▮▮▮⚝ 缺乏抽象:低层次的细节暴露在高层次代码中。
▮▮▮▮如何识别:代码难以阅读和理解,修改一个地方常常导致意想不到的副作用,新人难以快速上手。
▮▮▮▮如何避免/重构:坚持单一职责原则 (Single Responsibility Principle, SRP),进行模块化和组件化,引入分层架构,使用依赖注入 (Dependency Injection) 降低耦合。
② 瑞士军刀类 (Swiss Army Knife Class):
▮▮▮▮一个类提供了过多不相关的功能,试图成为“万能”工具。这违反了单一职责原则,使得类庞大且难以维护。
▮▮▮▮在C++中,这可能是一个包含大量静态工具方法 (Static Utility Methods) 的类,或者一个聚合了多种不同类型行为的类。
▮▮▮▮如何识别:类接口巨大,成员方法功能各异,类名不能清晰地描述其单一职责。
▮▮▮▮如何避免/重构:将不相关的功能拆分到不同的类中,每个类只负责一个特定的职责。
③ 魔术字符串/数字 (Magic Strings/Numbers):
▮▮▮▮直接在代码中使用字面常量字符串或数字,而没有赋予它们有意义的名称。这降低了代码的可读性和可维护性。
▮▮▮▮例如,使用硬编码的文件路径、错误码或配置值。
▮▮▮▮如何识别:代码中出现含义不明的字符串或数字常量。
▮▮▮▮如何避免/重构:使用具名常量(如const
变量、enum class
、宏定义),将配置信息放在配置文件中。
④ 循环依赖 (Circular Dependency):
▮▮▮▮两个或多个模块/类之间相互依赖,形成一个闭环。这使得模块难以独立编译、测试和复用,也可能导致内存泄漏问题(尤其在使用原始指针或引用计数不当的情况下)。
▮▮▮▮在C++中,这通常表现为头文件互相包含或类之间互相持有指针/引用。
▮▮▮▮如何识别:编译依赖图中有循环,或者在头文件中需要前向声明 (Forward Declaration) 但仍然难以打破依赖。
▮▮▮▮如何避免/重构:引入新的抽象层,使用依赖注入或事件机制解耦,考虑使用智能指针(如std::weak_ptr
打破循环引用)。
⑤ 复制-粘贴编程 (Copy-Paste Programming):
▮▮▮▮通过简单复制粘贴现有代码来添加新功能,而不是抽象出通用逻辑进行复用。这导致代码冗余,修改时需要同时更新多个地方,容易出错。
▮▮▮▮如何识别:代码库中存在大量相似或完全相同的代码段。
▮▮▮▮如何避免/重构:识别重复代码,抽象出函数、类、模板或设计模式来消除冗余,遵循DRY (Don't Repeat Yourself) 原则。
⑥ 硬编码 (Hard Coding):
▮▮▮▮将本应可配置或可变的数据直接写入代码中,而不是通过外部配置、参数或依赖注入来提供。
▮▮▮▮例如,数据库连接字符串、服务地址等。
▮▮▮▮如何识别:代码中包含特定环境或特定业务规则的固定值。
▮▮▮罚如何避免/重构:将配置数据外部化,使用配置文件、命令行参数或环境变量。
⑦ 死代码 (Dead Code):
▮▮▮▮永远不会被执行的代码。这增加了代码库的大小和阅读负担,也可能包含潜在的bug。
▮▮▮▮如何识别:通过静态分析工具或代码覆盖率分析发现。
▮▮▮罚如何避免/重构:定期进行代码清理,移除不再需要的代码。
8.1.2 反模式的识别与重构策略 (Identifying and Refactoring Anti-Patterns)
识别反模式通常需要经验和对代码库的深入理解。一些工具(如静态代码分析器、代码复杂度分析工具)可以提供帮助,但更重要的是通过代码评审 (Code Review) 和持续改进的文化来发现问题。
一旦识别出反模式,重构是解决问题的关键。重构 (Refactoring) 是指在不改变软件外部行为的前提下,改进其内部结构,使其更易理解和修改。利用设计模式常常是重构反模式的有效手段。
例如:
⚝ 将“大泥球”拆分为多个模块,每个模块可能遵循不同的设计模式。
⚝ 将“瑞士军刀类”拆分为多个职责单一的类,可能应用策略模式 (Strategy Pattern) 或状态模式 (State Pattern) 来管理不同的行为。
⚝ 将重复代码抽象为模板方法模式 (Template Method Pattern) 或策略模式 (Strategy Pattern) 中的通用算法骨架。
⚝ 使用工厂模式 (Factory Pattern) 或抽象工厂模式 (Abstract Factory Pattern) 来解耦对象的创建,从而打破某些循环依赖。
重构是一个持续的过程,应该小步进行,每次只关注一个特定的反模式或代码异味 (Code Smells),并通过自动化测试确保重构没有引入新的错误。
8.2 模式挖掘与领域特定模式 (Pattern Mining and Domain-Specific Patterns)
设计模式并非凭空创造,它们是对优秀软件设计经验的总结和抽象。模式挖掘(Pattern Mining)是指在现有成熟的软件系统或代码库中识别和提取有价值的设计模式。这有助于我们学习他人的经验,并将这些模式应用到自己的项目中。
除了通用的GoF设计模式,许多特定领域也形成了自己的设计模式,这些模式被称为领域特定模式 (Domain-Specific Patterns)。
8.2.1 从现有代码中发现模式 (Discovering Patterns in Existing Code)
通过模式挖掘,我们可以从成功的开源项目、框架或遗留系统中学习。这通常涉及:
① 代码阅读与分析:
▮▮▮▮深入阅读代码,理解其结构、类之间的关系以及交互方式。寻找重复出现的结构或解决方案。
▮▮▮▮关注点:
▮▮▮▮⚝ 类层次结构:是否存在明显的继承或组合结构?
▮▮▮▮⚝ 对象交互:对象如何相互调用和协作?是否存在中介者 (Mediator) 或观察者 (Observer) 模式的迹象?
▮▮▮▮⚝ 对象创建:对象是如何创建的?是否存在工厂 (Factory) 或建造者 (Builder) 模式的应用?
▮▮▮▮⚝ 接口使用:如何使用接口来处理不同的实现?是否存在策略 (Strategy) 或桥接 (Bridge) 模式?
② 结构和行为的可视化:
▮▮▮▮使用UML工具或其他可视化工具绘制类图、序列图等,帮助理解系统的静态结构和动态行为。这往往能更清晰地揭示隐藏的设计模式。
③ 与经验丰富的开发者交流:
▮▮▮▮请教熟悉代码库的资深开发者,他们可能已经意识到了其中使用的模式,或者能指出设计的关键思想。
④ 查阅文档和设计决策记录:
▮▮▮▮如果项目有设计文档,其中可能会记录设计时考虑的模式。即使没有明确提及模式名称,文档也可能解释设计背后的原因,从而帮助你识别模式。
模式挖掘是一个逆向工程 (Reverse Engineering) 的过程,它不仅能帮助你理解现有系统,还能加深你对设计模式的理解和应用能力。通过分析不同场景下同一个模式的不同实现,你可以学到模式的变体和适应性。
8.2.2 领域特定模式 (Domain-Specific Patterns)
GoF设计模式是通用的、语言无关的(尽管本书专注于C++实现)低层次模式,它们关注类和对象之间的关系。与此相对,领域特定模式关注在特定应用领域中反复出现的设计问题和解决方案。
例如:
⚝ 企业应用领域:如持久化层模式 (Persistence Layer Patterns),管理数据存储和访问;业务代表模式 (Business Delegate Pattern),解耦客户端和业务服务;数据传输对象 (Data Transfer Object, DTO) 模式,用于跨进程或跨层传输数据。
⚝ 游戏开发领域:如游戏循环 (Game Loop) 模式,控制游戏的主体流程;组件模式 (Component Pattern),用于构建灵活的游戏对象;状态机模式 (State Machine Pattern),管理游戏角色的不同状态。
⚝ 并发编程领域:如生产者-消费者模式 (Producer-Consumer Pattern),用于协调生产者和消费者线程;读写锁模式 (Reader-Writer Lock Pattern),允许多个读者或一个写者访问共享资源;消息队列模式 (Message Queue Pattern),用于线程或进程间的异步通信。
⚝ 图形界面领域:如模型-视图-控制器 (Model-View-Controller, MVC) 或模型-视图-视图模型 (Model-View-ViewModel, MVVM) 模式,用于组织UI代码。
学习领域特定模式非常重要,因为它们反映了特定领域的最佳实践。掌握这些模式能帮助你更快地理解和构建该领域的软件系统。这些模式可能建立在GoF模式之上,或者以不同的方式组合GoF模式来解决更具体的领域问题。
8.3 设计模式与软件架构模式 (Design Patterns and Software Architecture Patterns)
设计模式和软件架构模式(Software Architecture Patterns)都是解决软件设计问题的经验总结,但它们关注的层面不同。
8.3.1 模式的层次与范围 (Levels and Scope of Patterns)
① 设计模式 (Design Patterns):
▮▮▮▮关注类和对象层面的设计。它们描述了如何组织类、对象和它们之间的交互来解决特定的、相对局部的设计问题。GoF模式是典型的设计模式。它们提供了“微架构” (Micro-architecture) 级别的解决方案。
② 软件架构模式 (Software Architecture Patterns):
▮▮▮▮关注整个系统的宏观结构。它们定义了系统的基本组织结构、预定义的子系统集合以及它们之间的关系,并提供了组织整个软件系统的指导原则。架构模式是“宏架构” (Macro-architecture) 级别的。
可以类比为:设计模式是建筑中的“房间布局”、“窗户设计”、“门把手选择”等细节设计,而软件架构模式则是“房屋类型”(如独立别墅、公寓楼)、“整体结构”(如框架结构、砖混结构)、“功能分区”(如居住区、商业区)等整体规划。
8.3.2 常见的软件架构模式示例 (Examples of Common Software Architecture Patterns)
⚝ 分层架构 (Layered Architecture):
▮▮▮▮将系统划分为多个水平层,每层只依赖于其下层。常见的层包括表示层 (Presentation Layer)、业务逻辑层 (Business Logic Layer)、数据访问层 (Data Access Layer) 等。这有助于关注点分离 (Separation of Concerns)。
⚝ 客户端-服务器架构 (Client-Server Architecture):
▮▮▮▮将系统分为提供服务的服务器和请求服务的客户端。这是分布式系统的基本模式。
⚝ 模型-视图-控制器 (Model-View-Controller, MVC):
▮▮▮▮一种用于构建用户界面的架构模式,将应用分为数据模型 (Model)、用户界面 (View) 和处理用户输入的控制器 (Controller)。MVC本身也是一种设计模式(特别是组合模式、策略模式、观察者模式的应用),但其影响力巨大,常被视为一种架构模式。
⚝ 微服务架构 (Microservices Architecture):
▮▮▮▮将一个大型应用分解为一组小型、独立的服务,每个服务运行在其自己的进程中,通过轻量级机制通信。
⚝ 事件驱动架构 (Event-Driven Architecture):
▮▮▮▮系统组件通过产生、检测、消费和响应事件进行通信。
8.3.3 设计模式与架构模式的关系 (Relationship between Design Patterns and Architecture Patterns)
设计模式是实现软件架构模式的基础和构建块。在一个特定的架构模式下,系统中的每个子系统或组件内部的设计往往会使用一个或多个设计模式。
例如,在一个分层架构的业务逻辑层中,你可能使用策略模式 (Strategy Pattern) 来实现不同的业务规则,使用工厂模式 (Factory Pattern) 来创建业务对象。在一个微服务内部,你可能使用观察者模式 (Observer Pattern) 来处理内部事件,使用单例模式 (Singleton Pattern) 来管理资源。
理解不同层次的模式有助于我们在不同抽象级别上思考和解决问题。架构模式提供了系统的全局视图和组织原则,而设计模式提供了实现特定功能或组件的局部解决方案。优秀的软件设计需要在这两个层次上都做出合理的决策。
8.4 C++语言发展与设计模式 (C++ Evolution and Design Patterns)
C++语言自诞生以来一直在不断发展,引入了许多新的语言特性。这些新特性不仅提高了C++的表达能力和安全性,也对设计模式的实现方式产生了重要影响。现代C++ (Modern C++, 通常指C++11及之后版本) 提供了更强大、更简洁的工具来实现经典设计模式,甚至使得某些模式变得不再必要或有了更自然的替代方式。
8.4.1 现代C++特性对模式实现的影响 (Impact of Modern C++ Features on Pattern Implementations)
⚝ 智能指针 (Smart Pointers) (std::unique_ptr
, std::shared_ptr
, std::weak_ptr
):
▮▮▮▮彻底改变了资源管理和所有权语义。它们是RAII (Resource Acquisition Is Initialization) 习语的典型应用。在使用涉及对象生命周期管理的设计模式时(如单例模式 (Singleton Pattern)、组合模式 (Composite Pattern)、观察者模式 (Observer Pattern)),智能指针可以帮助避免内存泄漏和其他资源管理问题,简化代码。例如,在实现组合模式时,父节点持有子节点的智能指针可以自动管理内存。在观察者模式中,观察者持有主题的std::weak_ptr
可以避免循环引用。
⚝ 移动语义 (Move Semantics) 和 右值引用 (Rvalue References):
▮▮▮▮提高了对象复制的效率,特别是在处理大量数据或资源时。这影响了某些模式的实现,使得它们在传递大型对象时更加高效,例如在命令模式 (Command Pattern) 中存储复杂状态时。
⚝ Lambda表达式 (Lambda Expressions) 和 std::function
:
▮▮▮▮极大地增强了C++的函数式编程能力。它们可以作为一等公民 (First-class Citizens) 传递和存储函数对象。这为实现策略模式 (Strategy Pattern)、命令模式 (Command Pattern)、观察者模式 (Observer Pattern) 等行为型模式提供了新的、通常更简洁灵活的方式,有时可以替代虚函数和继承。
⚝ std::thread
和并发库:
▮▮▮▮C++11引入了标准的线程和并发工具。这对于实现涉及到并发的设计模式(如生产者-消费者、活跃对象 (Active Object) 等)至关重要。标准库提供了锁、条件变量等机制,帮助实现线程安全的单例模式 (Singleton Pattern) 或其他并发模式。
⚝ std::optional
, std::variant
, std::any
:
▮▮▮▮这些类型提供了更安全、更具表达力的方式来处理可选值、变体类型和任意类型。它们可以在某些场景下简化代码,例如在状态模式 (State Pattern) 中表示不同的状态数据,或者在命令模式 (Command Pattern) 中存储不同类型的参数。
⚝ 基于范围的 for 循环 (Range-based for loop):
▮▮▮▮简化了对容器元素的遍历,是迭代器模式 (Iterator Pattern) 在语言层面的直接支持和简化。
8.4.2 未来C++发展对设计模式的影响 (Future Impact of C++ Evolution on Design Patterns)
C++语言仍在不断发展,未来的特性如 Modules (模块)、Concepts (概念)、Coroutines (协程) 等可能会进一步影响设计模式的应用。
⚝ Modules:
▮▮▮▮模块系统将取代传统的头文件包含方式,有望显著改善编译时间并解决宏定义等带来的问题。这将使得依赖关系的管理更加清晰,可能影响某些依赖相关的设计模式(如外观模式 (Facade Pattern))的组织方式。
⚝ Concepts:
▮▮▮▮概念为模板编程提供了更好的约束和可读性,使得基于模板实现的设计模式(如Policy-based Design)更加健壮和易用。它能够更清晰地表达模板参数需要满足的要求。
⚝ Coroutines:
▮▮▮▮协程为编写异步和并发代码提供了新的范式。这可能会影响并发相关的设计模式,例如观察者模式 (Observer Pattern) 或其他涉及异步通知和任务调度的模式,使得其实现更加简洁和高效。
学习设计模式不仅要掌握经典模式的原理和实现,还要关注它们在现代C++环境下的最佳实践,并预测未来语言特性可能带来的影响。优秀的C++开发者能够灵活运用语言特性来实现模式,或者在某些情况下,利用语言特性来替代某些模式的经典实现。
8.5 总结与持续学习 (Conclusion and Continuous Learning)
恭喜你!阅读到这里,你已经对C++设计模式有了全面的了解,从基本概念到GoF经典模式,再到现代C++中的应用和进阶话题。设计模式是软件开发领域经过验证的宝贵经验,掌握它们能显著提升你的设计能力,帮助你构建更灵活、可维护、可扩展的C++软件系统。
8.5.1 本书内容回顾 (Book Content Recap)
本书从设计模式的起源和重要性出发,回顾了理解C++设计模式所需的OOP基础和现代C++特性。接着,我们系统地学习了23个GoF设计模式,按照创建型、结构型和行为型进行了分类和详细解析,包括它们的意图、结构、参与者、C++实现示例、优缺点、适用场景以及与其他模式的关系。
我们探讨了如何在实际C++项目中应用设计模式,结合现代C++特性优化实现,考虑性能,并利用设计模式进行代码重构。通过案例研究,我们演示了将多个模式组合应用于解决实际问题的过程。最后,我们讨论了反模式、模式挖掘、领域特定模式以及C++语言发展对设计模式的影响等进阶话题。
8.5.2 设计模式的持续学习与实践 (Continuous Learning and Practice of Design Patterns)
掌握设计模式是一个持续学习和实践的过程。阅读本书只是一个起点。要真正精通设计模式,你需要:
① 动手实践:
▮▮▮▮不要仅仅停留在理论层面。尝试在自己的项目或练习中应用设计模式。从简单的模式开始,逐步挑战更复杂的模式。
▮▮▮▮尝试用不同的方式实现同一个模式(例如,用继承、std::function
和Lambda实现策略模式)。
② 分析现有代码:
▮▮▮▮阅读高质量的开源C++代码,识别其中使用的设计模式。理解为什么作者选择了特定的模式,以及它是如何解决问题的。分析C++标准库、Boost库或其他知名库的源码,它们是设计模式应用的宝库。
③ 参与代码评审:
▮▮▮▮积极参与团队的代码评审,学习他人的设计思路,也让他人帮助你发现设计中的问题和改进点。
④ 学习领域特定模式:
▮▮▮▮关注你所工作的特定领域,学习该领域中常用的设计模式和架构风格。
⑤ 关注C++语言的新发展:
▮▮▮▮持续学习C++的新标准和新特性,理解它们如何影响现有的设计模式实践,以及如何利用它们更优雅地实现模式。
⑥ 阅读更多书籍和文章:
▮▮▮▮设计模式领域有许多经典的著作和最新的研究。阅读不同的资源可以帮助你从不同的角度理解模式。本书附录中提供了推荐读物。
⑦ 讨论和分享:
▮▮▮▮与同事或社区讨论设计模式的应用和挑战。分享你的经验,也从他人的经验中学习。
8.5.3 成为更优秀的设计师 (Becoming a Better Designer)
设计模式是工具,而不是目的。它们能帮助你思考设计问题,但并不能替代你的设计思考过程。成为一个优秀的设计师需要:
⚝ 理解问题领域:
▮▮▮▮深入理解你要解决的问题的本质和需求。
⚝ 权衡取舍:
▮▮▮▮没有完美的模式,每种模式都有其优缺点。你需要根据具体的上下文、需求和约束来选择最适合的模式,并权衡易用性、性能、复杂性等因素。
⚝ 注重简洁和清晰:
▮▮▮▮优秀的设计往往是简洁而清晰的。避免过度设计,不要为了使用模式而使用模式。
⚝ 拥抱变化:
▮▮▮▮软件是不断演进的。你的设计应该具有一定的灵活性,能够适应未来的变化。设计模式正是为了提高这种适应性而诞生的。
⚝ 持续学习和反思:
▮▮▮▮从失败和成功中学习,不断反思和改进你的设计实践。
通过持续的学习和大量的实践,你将能够更加自如地运用设计模式,提升你的软件设计能力,编写出高质量的C++代码,并在复杂的软件世界中游刃有余。祝你在C++设计模式的学习和应用之路上取得成功!
Appendix A: UML基础表示法 (Basic UML Notation)
本书在讲解设计模式时,为了清晰地表达模式的结构和交互,将大量使用统一建模语言(Unified Modeling Language, UML)图。本附录旨在提供一个快速参考,解释本书中会用到的UML基础表示法,特别是类图(Class Diagram)和序列图(Sequence Diagram)的关键元素。掌握这些基础知识,将有助于您更好地理解书中对设计模式的图形化描述。
Appendix A1: 类图 (Class Diagram)
类图是UML中最常用的一种图,用于静态地表示系统中的类、接口以及它们之间的关系。它展示了系统的结构,是理解设计模式核心结构的关键。
Appendix A1.1: 类 (Class) 的表示
一个类通常用一个矩形框表示,框内分成三个区域:
⚝ 顶部区域: 类名(Class Name)。通常居中对齐。抽象类(Abstract Class)的名称通常用斜体表示。
⚝ 中间区域: 类的属性(Attributes)。格式通常是:[可见性] 属性名: 类型 [= 默认值]
。
⚝ 底部区域: 类的方法/操作(Operations)。格式通常是:[可见性] 方法名(参数列表): 返回类型
。抽象方法通常也用斜体表示。
可见性 (Visibility) 符号:
⚝ +
: Public (公共)
⚝ -
: Private (私有)
⚝ #
: Protected (保护)
⚝ ~
: Package (包) - 在C++语境中通常不常用,但了解其含义有益。
例如:
1
+-----------------------+
2
| ClassName |
3
+-----------------------+
4
| - privateAttribute: int|
5
| # protectedAttr: double|
6
| + publicAttr: string |
7
+-----------------------+
8
| + publicMethod(): void |
9
| - privateMethod(int): bool|
10
| # protectedMethod(): SomeType|
11
+-----------------------+
Appendix A1.2: 接口 (Interface) 的表示
接口用于定义一组操作的契约(Contract),而不包含实现。在UML中,接口可以用以下两种方式表示:
① 使用带有 <<interface>>
构造型(Stereotype)的标准类矩形框。
② 使用一个圆圈(称为棒糖表示法,Lollipop Notation),通常与实现该接口的类相连。
例如:
1
+-----------------------+ O
2
| <<interface>> | /
3
| IComparable | /
4
+-----------------------+ /
5
| + compareTo(Object): int | /
6
+-----------------------+ /
7
| /
8
| Implements /
9
| /
10
+-----------------------+ /
11
| MyClass |
12
+-----------------------+
13
| |
14
+-----------------------+
15
| + compareTo(Object): int |
16
+-----------------------+
Appendix A1.3: 关系 (Relationships) 的表示
类之间通过关系连接,表示它们如何相互作用。本书将重点介绍以下几种关系:
① 关联 (Association):
⚝ 表示类之间的结构化关系,例如一个类包含另一个类的实例作为成员变量。
⚝ 用实线表示,可以加上角色名(Role Name)和多重性(Multiplicity)。
⚝ 多重性示例:1
(恰好一个), 0..1
(零或一个), *
(零或多个), 1..*
(一个或多个), m..n
(m到n个)。
1
+---------+ 1..* +---------+
2
| Order |-----------------| LineItem|
3
+---------+ +---------+
4
| | owns | |
5
+---------+ +---------+
② 聚合 (Aggregation):
⚝ 一种特殊的关联,表示“部分-整体”关系,但部分可以独立于整体存在。
⚝ 用带有空心菱形的实线表示,菱形位于整体一侧。
1
+---------+ <#>-------+---------+
2
| Window | | Panel |
3
+---------+ +---------+
③ 组合 (Composition):
⚝ 一种更强的“部分-整体”关系,表示部分的生命周期依赖于整体,整体被销毁时部分也随之销毁。
⚝ 用带有实心菱形的实线表示,菱形位于整体一侧。
1
+---------+ <*>-------+---------+
2
| Car | | Engine |
3
+---------+ +---------+
④ 泛化/继承 (Generalization/Inheritance):
⚝ 表示一个类(子类/派生类)继承自另一个类(父类/基类)。
⚝ 用带有空心箭头的实线表示,箭头指向父类。
1
+---------+
2
| Base |
3
+---------+
4
▲
5
|
6
| Inheritance
7
|
8
+---------+
9
| Derived |
10
+---------+
⑤ 依赖 (Dependency):
⚝ 表示一个类(客户端)使用了另一个类(供应商)的功能,例如一个类的方法中使用了另一个类的对象、传递另一个类的对象作为参数或返回类型。依赖关系通常是临时的或使用关系,一个类的改变可能影响依赖它的类。
⚝ 用带有箭头的虚线表示,箭头指向被依赖的类。
1
+---------+ + - - - - > +---------+
2
| Client | Uses | Service |
3
+---------+ +---------+
⑥ 实现 (Realization/Implementation):
⚝ 表示一个类实现了接口中定义的操作。
⚝ 用带有空心箭头的虚线表示,箭头指向接口。
1
+---------+ + - - - - ▲
2
| MyClass | Implements|
3
+---------+ |
4
+-----------------+
5
| <<interface>> |
6
| IMyInterface|
7
+-----------------+
Appendix A2: 序列图 (Sequence Diagram)
序列图用于表示对象之间随着时间的推移的交互,特别是方法调用和消息传递的顺序。它们是理解设计模式行为和运行时协作的关键。
Appendix A2.1: 参与者/对象 (Participants/Objects)
⚝ 序列图顶部的矩形框表示参与交互的对象或角色。
⚝ 对象名通常是 对象名: 类名
或只有 类名
(如果只涉及一个该类的实例) 或只有 对象名
(如果类不重要)。
⚝ 从对象框下方延伸出一条垂直的虚线,称为生命线(Lifeline),表示对象在某个特定时间段内的存在。
1
+-------+ +-------+ +-------+
2
| Client| | ServiceA| | ServiceB|
3
+-------+ +-------+ +-------+
4
| | |
5
| | |
Appendix A2.2: 消息 (Messages)
⚝ 对象之间通过消息传递进行通信,通常表示方法调用。
⚝ 消息用带有箭头的实线表示,从发送者对象的生命线指向接收者对象的生命线。
⚝ 消息线上通常标有方法名及其参数。
⚝ 同步消息 (Synchronous Message): 表示调用者等待方法执行完成并返回结果。用实心箭头表示 ▶。
⚝ 异步消息 (Asynchronous Message): 表示调用者发送消息后立即继续执行,不等待接收者完成。用开放式箭头表示 >。
⚝ 返回消息 (Return Message): 表示同步消息的返回值。用带有开放式箭头的虚线表示 --->。通常省略,因为同步消息隐含了返回。
1
+-------+ +-------+
2
| Client| | Service|
3
+-------+ +-------+
4
| |
5
| ▶ callMethod(param) |
6
| |
7
| <--- result |
8
| |
Appendix A2.3: 生命周期与激活 (Lifeline and Activation)
⚝ 生命线(Lifeline)是对象存在期间的时间线。
⚝ 激活(Activation)或执行规范(Execution Specification)表示对象正在执行操作的时期。
⚝ 激活用生命线上的窄矩形条表示。当对象被调用时开始,当方法返回时结束。
1
+-------+ +-------+
2
| Client| | Service|
3
+-------+ +-------+
4
| |
5
| ▶ callMethod() |
6
|▮▮▮▮▮▮▮▮▮▮▮ |-------+
7
|▮▮▮▮▮▮▮▮▮▮▮ |▶ doSomething() |
8
|▮▮▮▮▮▮▮▮▮▮▮ |▮▮▮▮▮▮▮▮▮▮▮ |
9
|▮▮▮▮▮▮▮▮▮▮▮ |<--- return |
10
|▮▮▮▮▮▮▮▮▮▮▮ |-------+
11
|<--- return |
12
| |
Appendix A2.4: 组合片段 (Combined Fragments)
组合片段用于表示更复杂的控制结构,如条件、循环和可选行为。它们用一个方框表示,左上角标有操作符(Operator)。
⚝ 可选 (Optional): opt
操作符。包含在一个片段中,表示该片段在满足特定条件时执行。
1
opt [条件]
2
+------------------+
3
| ▶ messageIfTrue |
4
+------------------+
⚝ 替代 (Alternative): alt
操作符。用于表示 if-then-else 或 switch 结构。片段之间用水平虚线 --
分隔。
1
alt
2
+------------------+
3
| [条件1] |
4
| ▶ messageIfTrue1 |
5
+------------------+
6
--
7
+------------------+
8
| [条件2] |
9
| ▶ messageIfTrue2 |
10
+------------------+
11
--
12
+------------------+
13
| [否则] |
14
| ▶ messageElse |
15
+------------------+
⚝ 循环 (Loop): loop
操作符。表示一个片段将重复执行。循环条件可以放在方括号中。
1
loop [循环条件]
2
+------------------+
3
| ▶ messageInLoop |
4
+------------------+