导读:本文将讲解两种经典设计模式——简单工厂模式和策略模式,并展示如何将它们结合使用。通过生动的场景代入和UML类图,帮助你理解从设计到代码的完整过程。
学习设计模式最好的方法就是把它代入到真实生活的场景中。作为初学者,你完全不需要害怕这些高大上的名词。
一、简单工厂模式
简单工厂模式(Simple Factory Pattern),顾名思义,它的核心思想就是:把”创建对象”的脏活累活交给一个专门的”工厂”来做,使用者只需要”伸手要”就行了。 💡
为了让你生动地理解,我们再次回到经典的香港动作电影片场。
1. 生动场景代入:片场的“道具组”
假设现在正在拍摄一部由刘德华主演的警匪动作大片。在接下来的几场戏中,导演要求男主角分别使用重型摩托车和跑车来进行追逐。
- 没有工厂模式的情况: 演员(代码中的业务调用者)需要自己去满大街找零件、自己组装引擎、自己造出一辆摩托车(也就是在代码里到处写
new Motorcycle())。这显然极其荒谬,演员的职责是演戏,不是造车! - 使用简单工厂模式: 片场设立了一个**“道具工厂(PropFactory)”**。演员只需要对道具总监喊一句:“给我一辆摩托车!”道具工厂就会在后台把车准备好,直接把车钥匙交到演员手里。演员根本不需要知道这辆车是怎么造出来的。
2. UML 类图与角色拆解
在简单工厂模式中,通常有三个核心角色:
- 抽象产品(Product 接口): 所有道具的”父类”或”接口”。比如定义一个
Vehicle(交通工具),规定所有车都必须能drive()。 - 具体产品(Concrete Product): 真正被造出来的东西,继承自抽象产品。比如
Motorcycle(摩托车)和SportsCar(跑车)。 - 工厂(Factory): 核心类,负责根据传入的指令(如字符串),决定并
new出哪一个具体产品的实例。
简单工厂模式 UML 类图
classDiagram
class Vehicle {
<>
+drive()*
}
class Motorcycle {
+drive()
}
class SportsCar {
+drive()
}
class PropFactory {
+createVehicle(type: String)$ unique_ptr~Vehicle~
}
class Client {
+main()
}
Vehicle <|-- Motorcycle : 泛化
Vehicle <|-- SportsCar : 泛化
PropFactory ..> Motorcycle : 依赖 (创建)
PropFactory ..> SportsCar : 依赖 (创建)
PropFactory ..> Vehicle : 依赖 (返回)
Client ..> PropFactory : 依赖
Client ..> Vehicle : 依赖
UML 关系解读:
- 工厂(PropFactory)依赖具体的产品类(虚线箭头),因为它要去
new它们。- 具体产品(Motorcycle 等)泛化/实现了抽象产品(实线/虚线空心三角箭头)。
- 演员(调用者)只依赖工厂和抽象产品,完全不和具体产品打交道(解耦)。
3. C++ 代码生动演绎
我们将上面的片场故事转化为现代 C++ 代码(使用我们之前学过的 std::unique_ptr 来安全地管理内存):
1 |
|
4. 简单工厂的优缺点
没有任何设计模式是完美的,了解它的利弊才能用在刀刃上。
优点:
- 分工明确,降低耦合: 对象的使用者(演员)和对象的创建者(道具厂)彻底分开了。使用者不需要去记那些复杂的类名(比如
HexahedronElement_V2_Optimized),只需要记住一个简单的字符串(”摩托车”)就能拿到对象。 - 封装创建逻辑: 如果创建一个对象需要很复杂的初始化步骤(比如连接数据库、读取配置文件),把这些代码全塞进工厂里,可以保持业务代码的干净整洁。
缺点(致命伤):
- 违反了“开闭原则”(Open-Closed Principle): 这是面向对象设计的核心原则,意思是对扩展开放,对修改关闭。
- 假设现在导演突然要加一场直升机追逐戏。 我们必须打开
PropFactory类的源代码,在里面硬生生地加上一段else if (type == "直升机")。每次增加新产品都要修改工厂核心代码,这在大型工程中极易引发 BUG。
- 假设现在导演突然要加一场直升机追逐戏。 我们必须打开
- 工厂类可能变成“上帝类”(God Class): 如果你的软件有几十种产品,那个工厂里的
if-else分支会变得长得可怕,难以维护。
5. 什么情况下应该使用简单工厂?
结合它的优缺点,简单工厂模式非常适合以下场景:
- 创建的对象种类较少,且相对固定: 比如你写一个计算器程序,工厂只需要造出“加、减、乘、除”四种运算对象即可,基本不会再变了。
- 客户端不关心对象是如何创建的: 调用方只在乎最后拿到的接口能不能用,完全不在乎初始化过程。
- 作为初学者重构代码的第一步: 如果你发现你的
main函数里或者业务代码里到处都是一坨一坨的new XXX(),把它们提取到一个工厂类里,会让你的代码瞬间从“初级”提升到“有架构思维”的层次。
在实际的大型 C++ 工程(比如 FITK 这样的底层算法框架)中,如果产品种类繁多且不断扩展,我们通常会抛弃简单工厂,升级使用**工厂方法模式(Factory Method)或者抽象工厂模式(Abstract Factory)**来完美解决那个烦人的 if-else 问题。
🔜 接下来,让我们看看另一种经典模式——策略模式,它解决的是”算法替换”的问题。
二、策略模式
策略模式(Strategy Pattern) 是面向对象设计模式中最实用、也是你以后在底层算法开发中最常打交道的模式之一! 🎯
如果说“简单工厂模式”是为了解决**“对象怎么被创建出来”的问题,那么“策略模式”就是为了解决“对象的方法该怎么执行(算法的替换)”**的问题。
它的核心思想是:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。策略模式使得算法可独立于使用它的客户而变化。
为了让你秒懂,我们直接代入你熟悉的 FITK 软件中的拓扑优化(Topology Optimization) 场景。
1. 生动场景代入:拓扑优化的“算法库”
在编写拓扑优化器时,整个迭代的宏观流程是固定的:
- 有限元分析 (FEA) 求位移和应变。
- 灵敏度分析求导数。
- 更新材料密度 (Density Update)。
- 检查是否收敛。
这其中,第 3 步**“更新材料密度”**是核心算法所在。目前学术界和工业界有多种优秀的算法,比如最经典的 OC 法(优化准则法,Optimality Criteria),以及更高级的 MMA 法(移动渐近线法,Method of Moving Asymptotes)。
没有策略模式的情况: 你的
TopologyOptimizer类里面会写满又臭又长的if-else。1
2
3
4
5if (method == "OC") {
// 执行 500 行 OC 算法的代码
} else if (method == "MMA") {
// 执行 1000 行 MMA 算法的代码
}如果以后还要加 GCMMA、BESO 法,这个类会膨胀到无法维护,且极容易改出 BUG。
使用策略模式: 我们把“密度更新”定义为一个接口(契约)。OC 法和 MMA 法分别是具体的**“策略”**。优化器(Optimizer)手里只握着策略接口的遥控器。用户想用什么算法,就在运行前把哪个“策略芯片”插进优化器里。优化器运行时,盲按遥控器即可。
2. UML 类图与角色拆解
策略模式通常包含三个核心角色:
- 抽象策略(Strategy 接口): 定义了所有支持的算法的公共接口。在 C++ 中是含有纯虚函数的抽象类(对应 UML 中的
<<interface>>)。 - 具体策略(Concrete Strategy): 实现了抽象策略接口的具体算法(如 OC 法、MMA 法)。它们与接口之间是实现/泛化关系。
- 上下文(Context): 用来操作策略的类(如
TopologyOptimizer)。它内部维护一个指向抽象策略的指针。它与策略接口之间是聚合或组合关系(取决于生命周期管理)。
策略模式 UML 类图
classDiagram
class IUpdateStrategy {
<>
+executeUpdate(densities: vector~double~)*
}
class OcStrategy {
+executeUpdate(densities: vector~double~)
}
class MmaStrategy {
+executeUpdate(densities: vector~double~)
}
class TopologyOptimizer {
-taskName: String
-currentDensities: vector~double~
-updateStrategy: unique_ptr~IUpdateStrategy~
+TopologyOptimizer(name: String)
+setStrategy(newStrategy: unique_ptr~IUpdateStrategy~)
+runIteration()
}
class Client {
+main()
}
IUpdateStrategy <|.. OcStrategy : 实现
IUpdateStrategy <|.. MmaStrategy : 实现
TopologyOptimizer *-- IUpdateStrategy : 组合
Client ..> TopologyOptimizer : 依赖
Client ..> OcStrategy : 依赖 (创建)
Client ..> MmaStrategy : 依赖 (创建)
3. C++ 代码生动演绎
我们用现代 C++ 代码把这个拓扑优化场景写出来,体会一下在运行时”热插拔”算法的快感:
1 |
|
4. 策略模式的优缺点
优点:
- 完美契合“开闭原则”: 当未来学术界发表了牛逼的全新算法(比如某种基于深度学习的更新策略),你只需要新建一个类去继承
IUpdateStrategy即可。TopologyOptimizer核心类的代码一行都不用改! - 消灭了大量的
if-else: 代码结构变得极其清爽,每个算法的逻辑都被隔离在自己的类里,测试和排错非常方便。 - 运行时动态切换: 就像代码里展示的那样,程序跑着跑着,只要调用
setStrategy()就可以瞬间换一套算法。
缺点:
- 调用方必须了解所有策略: 客户(上层写界面的程序员或者最终用户)必须知道 OC 和 MMA 的区别,才能决定给优化器装配哪个策略。
- 增加了类的数量: 每一个小算法都会产生一个新的
.h和.cpp文件。
5. 什么情况下使用策略模式?
- 当一个系统需要动态地在几种算法中选择一种时。(比如:不同的排序算法、不同的路径规划算法、不同的文件压缩算法、不同的拓扑优化求解算法)。
- 当一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句(巨型
if-else或switch)的形式出现。(把条件分支转化为独立的策略类)。 - 当你不想暴露复杂的、特定于算法的数据结构时。(算法内部怎么算雅可比矩阵,外面根本不需要看)。
📝 本节小结
| 维度 | 简单工厂模式 | 策略模式 |
|---|---|---|
| 解决的问题 | 如何创建对象 | 如何替换算法 |
| 核心思想 | 把创建逻辑封装到工厂 | 把算法封装到策略类 |
| UML关系 | 工厂依赖具体产品 | 上下文组合策略接口 |
| 优点 | 隐藏创建细节 | 运行时动态切换算法 |
| 缺点 | 违反开闭原则 | 客户端需了解所有策略 |
💡 一句话总结:简单工厂是用来**“生孩子(造对象)”的,而策略模式是用来“教孩子做事(选算法)”**的。
🔜 接下来,让我们看看如何将这两种模式强强联手,打造更优雅的架构!
三、模式结合:策略模式 + 简单工厂
你提出了一个非常进阶且极具实战价值的问题!在实际的 C++ 底层架构开发中,我们很少孤立地使用某一种设计模式,真正的架构师都是**“打组合拳”** 🥊。
把“策略模式”和“简单工厂模式”结合在一起,是重构代码时最经典、最爽快的招式。在揭晓这种结合方法之前,我们先来回答你关于**“客户端(使用者)需要知道哪些类”**的灵魂拷问,因为这正是促使我们把这两个模式结合起来的根本动机。
1. 灵魂拷问:作为使用者,我到底需要知道谁?
假设你就是那个在 main 函数里写代码、调用底层模块的程序员(客户端):
- 在使用【简单工厂模式】时,你需要知道:3 个东西。
- 工厂类(
PropFactory) - 抽象接口(
Vehicle,因为你要用它来接收对象) - 一个暗号字符串(比如
"摩托车")
- 你不知道的: 具体的子类(
Motorcycle)。你完全被蒙在鼓里,极其省心。
- 工厂类(
- 在使用【纯正的策略模式】时,你需要知道:3 个东西(甚至更多)。
- 上下文类(
TopologyOptimizer) - 抽象策略接口(
IUpdateStrategy) - 所有的具体策略类(
OcStrategy、MmaStrategy…)
- 发现痛点了吗? 策略模式的缺点在于,它把选择算法的重担完全扔给了使用者。你作为调用方,必须
#include所有的具体算法头文件,亲自new OcStrategy()才能传给优化器。
- 上下文类(
2. 强强联手:策略模式 + 简单工厂
既然简单工厂能把具体的类隐藏起来,而策略模式能把算法的执行隔离开来,那为什么不把它们合体呢?
结合模式 UML 类图
classDiagram
class IUpdateStrategy {
<>
+executeUpdate(densities: vector~double~)*
}
class OcStrategy {
+executeUpdate(densities: vector~double~)
}
class MmaStrategy {
+executeUpdate(densities: vector~double~)
}
class TopologyOptimizer {
-taskName: String
-currentDensities: vector~double~
-updateStrategy: unique_ptr~IUpdateStrategy~
+TopologyOptimizer(name: String, methodType: String)
+runIteration()
}
class Client {
+main()
}
IUpdateStrategy <|.. OcStrategy : 实现
IUpdateStrategy <|.. MmaStrategy : 实现
TopologyOptimizer *-- IUpdateStrategy : 组合
TopologyOptimizer ..> OcStrategy : 依赖 (内部创建)
TopologyOptimizer ..> MmaStrategy : 依赖 (内部创建)
Client ..> TopologyOptimizer : 依赖
📌 关键变化:客户端只需要知道
TopologyOptimizer和字符串参数,完全不需要了解OcStrategy和MmaStrategy的存在!
结合的方法非常巧妙:让“上下文(Context)”自己兼任“简单工厂”的角色!
具体做法是:我们不再让使用者在外面创建策略对象并传进来;相反,我们让使用者直接把“暗号字符串”传给上下文的构造函数。上下文在自己初始化的同时,在内部偷偷把具体的策略创建好。
C++ 代码生动演绎(小米科技拓扑优化办公室的最终进化版)
看看结合后的代码,业务调用端(main 函数)会变得多么干净:
1 |
|
3. 最终总结对比表格
通过结合,我们将使用者的认知负担降到了最低:
| 模式名称 | 客户端需要知道的类/信息 | 客户端需要做的事 | 优缺点总结 |
|---|---|---|---|
| 纯简单工厂 | Factory、Interface、字符串 |
找工厂要对象,拿到后自己调用方法 | 隐藏了具体类的创建,但不够灵活 |
| 纯策略模式 | Context、Interface、所有具体策略类 |
自己 new 出具体的策略对象,喂给上下文 |
算法完美隔离、热插拔,但向外界暴露了太多具体的策略类 |
| 策略 + 简单工厂 | 仅仅只有 Context、字符串 |
告诉上下文想要哪种口味,上下文自己搞定内部一切 | ✅ 完美! 既隔离了算法,又隐藏了具体类,客户端代码做到极致精简 |
📊 全文总结
| 维度 | 简单工厂模式 | 策略模式 | 结合模式 |
|---|---|---|---|
| 解决的问题 | 如何创建对象 | 如何替换算法 | 创建 + 替换 |
| 核心调用 | 工厂.create() |
context.setAlgo() |
context("算法名") |
| 执行流程 | 返回产品对象 | 执行算法 | 自动创建并执行 |
| 优点 | 隐藏创建细节 | 运行时热插拔 | 双重隐藏 |
| 缺点 | 违反开闭原则 | 暴露具体策略类 | 仍违反开闭原则 |
💎 设计模式的魅力就在于这种举一反三的重组。既然你已经掌握了如何把策略和工厂结合,那么随着系统越来越复杂(比如不仅仅要挑选密度更新算法,还要挑选敏度过滤算法、网格划分器),要不要了解一下比简单工厂更强大、能彻底解决
if-else噩梦的抽象工厂模式 (Abstract Factory) 呢?
🎉 恭喜你! 你已经掌握了两种经典设计模式及其结合使用。继续加油,成为架构设计的高手!
- 本文作者: 迪丽惹Bug
- 本文链接: https://lyroom.github.io/2026/04/27/简单工厂模式和策略模式详解/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!