工厂方法模式如何体现开闭原则?
简化版
工厂方法把产品创建抽象成接口或抽象方法,新增产品时通过新增具体产品和具体工厂完成扩展,尽量不修改已有创建逻辑。它用多态替代集中条件分支,从而降低新增类型对旧代码的影响。
详细版
在简单工厂中,新增产品通常要修改类似 create(type) 的方法:
if (type == A) return new A();
if (type == B) return new B();
if (type == C) return new C();
这违反开闭原则的风险在于,每次添加 C 都要动原方法,可能破坏 A、B 的创建逻辑。
工厂方法会把创建行为抽象出来:
interface ReportFactory {
Report createReport();
}
class PdfReportFactory implements ReportFactory {
public Report createReport() {
return new PdfReport();
}
}
新增 Excel 报表时,可以新增 ExcelReport 和 ExcelReportFactory。原有 PdfReportFactory 不需要修改,调用方只依赖 ReportFactory 和 Report 抽象。
不过开闭原则不是“永远不修改任何代码”。系统仍然需要某个地方完成工厂选择,例如配置文件、依赖注入容器或注册表。工厂方法的价值是把产品创建规则的变化隔离在新类型中,而不是堆到一个中心分支里。
完整版教学
一、开闭原则关注变化影响面
开闭原则真正关心的是变化发生时,旧代码是否需要频繁被修改。简单工厂把变化集中在一个方法里,刚开始清晰,后面产品多了就容易变成高风险热点。
工厂方法把变化拆到具体工厂类中。新增产品时,新增代码多于修改代码,旧路径被误伤的概率更低。
二、多态如何替代分支
条件分支是在运行时根据类型判断做不同创建;多态是把不同创建逻辑放进不同对象,让对象自己完成创建。二者都能工作,但多态的扩展单位更清晰。
ReportFactory factory = new PdfReportFactory();
Report report = factory.createReport();
调用方不关心具体工厂内部怎么 new,只需要面对稳定的 ReportFactory 接口。
三、工厂选择仍然要有入口
一个常见误解是以为用了工厂方法就没有任何分支了。实际上系统总要决定使用哪个具体工厂,只是这个决策可以放在更合适的地方:
- 配置文件选择工厂实现;
- Spring 容器按 Bean 名称或条件注入;
- 注册表根据 key 找到工厂;
- 插件系统在启动时扫描并注册工厂。
这样可以把“选择工厂”和“创建产品”分开,避免一个方法既做路由又做复杂构造。
四、不是所有场景都需要工厂方法
如果产品只有一种,或者新增产品几乎不会发生,用普通构造器或简单工厂即可。工厂方法适合变化点明确、具体产品会持续扩展、创建过程不想暴露给调用方的场景。
模式的收益必须覆盖新增 Creator 类型、装配配置和导航跳转的成本。若一年内产品从未增加、构造器只有一行,直接依赖接口并由容器注入实现,往往已经提供足够的替换能力。
五、开闭原则要看“稳定模块”是否被反复打开
从 PDF 报表扩展 Excel 报表时,新增两个类并不自动等于优秀设计;关键是报表使用流程是否只依赖 Report,工厂选择是否集中在组合根,以及旧 PDF 创建和测试是否无需改动。
ReportService -> ReportFactory 接口
PdfReportFactory -> PdfReport
新增 ExcelReportFactory -> ExcelReport
配置 provider=excel -> 选择 Excel 工厂
ReportService.generate() 不变
PdfReportFactory 不变
旧 PDF 契约测试继续通过
这段协作图要回答两个不同问题:产品由谁构造,具体工厂又由谁选择。工厂方法可以把产品构造从业务流程中移走,但应用入口、配置层或容器仍然必须决定使用哪个工厂;声称模式“消灭了所有分支”并不准确。
六、模式边界与扩展成本
| 维度 | 本题结论 |
|---|---|
| 稳定抽象 | Report 的生成能力与 ReportFactory.createReport() 创建契约。 |
| 主要变化点 | 报表具体格式及其构造依赖。 |
| 新增一种产品的动作 | 新增具体 Report 和 Factory,在系统边界选择/注册。 |
| 不应放进工厂的职责 | 报表业务流程、权限判断和下载响应组装。 |
| 更轻或更合适的方案 | 产品数量固定时简单工厂更低成本;容器条件装配可承担启动期选择。 |
是否符合开闭原则不能只数新增了多少类,而要看高风险旧代码是否仍被反复修改。若每次扩展仍要修改一个中心 switch、产品接口又泄漏具体类型,即使类名叫 Factory,也没有获得工厂方法的主要收益。
七、用三个测试验证设计没有走样
- 契约测试:对每个具体工厂执行同一组产品接口测试,确认返回值满足抽象契约。
- 扩展测试:加入一个假产品和假工厂,记录为了接入它修改了哪些旧文件。
- 未知类型测试:若存在注册表或参数路由,明确缺失 key 的异常类型和降级策略。
- 生命周期测试:连续创建两次,确认产品应当复用还是隔离,避免无意变成单例或重复昂贵连接。
- 依赖测试:具体构造参数应由工厂显式获得,业务类不应通过全局容器偷偷查找。
- 并发测试:注册表若支持运行期写入,必须定义发布、覆盖和读取的一致性;只读注册表则尽量启动期冻结。
- 可观测性测试:记录命中的工厂 key 与具体实现,排查配置选择错误时无需猜测。
记忆钩子:开闭不是零修改,而是把必要修改推到低风险组合边界。
八、常见误区与追问
- 误区:开闭原则要求一行旧代码都不能改。 现实系统仍需配置或注册入口,目标是控制修改影响面。
- 误区:类越多越符合开闭原则。 若抽象不稳定,增加类型只会增加维护成本。
- 误区:工厂方法会自动消除所有选择逻辑。 具体工厂仍要在组合根、配置或注册表中被选择。
- 追问:新增产品时允许改哪里? 优先只在扩展类和低风险装配层增加配置或注册。
- 追问:怎样量化模式收益? 比较新增产品修改文件数、旧路径回归范围和冲突热点。
- 追问:简单工厂一定违反开闭原则吗? 不是绝对;产品稳定时集中分支的简单性可能更有价值。
九、加强记忆
工厂方法体现开闭原则的关键,是把新增产品从“修改中心工厂分支”变成“新增具体产品和工厂”。它不是让系统完全没有选择逻辑,而是让创建逻辑的变化有更稳定的隔离边界。