先说一个我在代码评审里经常遇到的画面:业务代码里到处都是new,一个订单创建逻辑里手写四五个if分支选择不同的子类,或者一个报表模块里反复地new同一个大对象,每次都要重新查库、重新组装几十个字段。有人提议用设计模式,结果又被怼了一句“过度设计”。实际上,工厂方法模式和原型模式恰恰是创建型模式里最容易被误解、也最容易被用到错误地方的两个。这篇文章我打算把这两个模式的定义、结构、核心思想、典型适用场景完整总结一遍,再用贴近真实业务的例子说明怎么落地、怎么避坑,适合正在啃设计模式、准备面试,或者在项目里纠结“要不要用工厂/能不能用原型”的开发者。
1. 先弄清楚:这两个模式解决的是同一个什么问题
很多人学设计模式容易陷入一个误区:把每个模式当成独立的招式和公式去背。其实创建型模式解决的是同一个问题——对象怎么创建。只是在“谁来创建、怎么创建、什么时候创建”这些问题上,每个模式给出的答案不同,于是分化出了各自的适用场景。
1.1 创建型模式的共同逻辑
我们先看一个最简单的场景:一个服务里需要用到某个具体实现类。
java复制public class OrderService {
private final OrderRepository repository = new MysqlOrderRepository();
}
这段代码看起来没什么问题,但一旦MysqlOrderRepository的构造函数需要传数据源、需要从配置文件读取连接池参数,或者明天要切换成OracleOrderRepository,你就得改动OrderService。改代码不是错,错的是改的时候你可能牵连到所有依赖它的上层逻辑。创建型模式的核心价值,就是把这种“依赖变化”隔离起来,让上层只依赖一个稳定的抽象。
工厂方法模式的思路是:定义一个创建对象的接口,让子类决定实例化哪一个类。它不是把所有创建逻辑集中到一个地方,而是把“选择哪个类来实例化”这件事推给子类。正因为如此,它的扩展方式是加子类,不是改原代码。
原型模式的思路完全不同:通过复制一个已有实例来创建新对象。它不依赖构造函数,也不需要知道具体类型,直接对着一个“模板对象”拷贝一份,拷贝出来的新对象和模板对象互不干扰(至少深拷贝下如此)。也就是说,工厂方法回答的是“该创建哪个类的实例”,原型回答的是“怎样快速复制一个已有的好东西”。
1.2 工厂方法和原型模式的本质差异
这里我打一个生活化的比方:去餐厅点菜。
工厂方法模式像看菜单点菜——你告诉厨房想要哪种菜,厨房根据你的选择决定用哪套流程做出来。客户端不需要关心食材怎么采购、火候怎么控制,只需要知道菜单上有哪些菜可选。
原型模式像下午茶点了一块蛋糕,吃完觉得不错,想让店员按照同一块蛋糕再复制一份。关键在于:蛋糕师不需要重新称面粉、打鸡蛋,直接照着已经做好的样品复制就行。如果蛋糕上有层奶油,复制的时候是把奶油涂层共享还是连奶油一起重做,就对应着浅拷贝和深拷贝的区别。
这个比方基本能解释清楚两个模式的本质差异:一个偏创建决策,一个偏复制效率。后面所有代码、场景、结构拆解,都是在这两个基础上加深。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 工厂方法模式:定义、结构与核心思想
2.1 定义中的三个关键词
官方定义是这样一句话:定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method让一个类的实例化延迟到其子类。
这句话里有三个关键词:
- 接口:创建者(Creator)不是直接把创建逻辑写在业务代码里,而是暴露一个工厂方法。
- 子类决定:真正“new谁”的动作发生在具体子类中。
- 延迟到子类:这个“延迟”不是时间上的延迟,而是将职责从父类/调用方转移给子类,让高层模块不需要早早就绑定到具体类上。
对比简单工厂,工厂方法看起来只是多了一层继承,但意义完全不同。简单工厂本质上是把“switch/if判断放在一个工具类里”,客户端还是要依赖这个工具类,工具类里的分支一变,所有调用方都可能受牵连。工厂方法则把分支判断下沉到一个个具体工厂子类里,新增产品时不需要改动已有的创建逻辑,只需要增加新的产品类和新的工厂子类。
2.2 四个角色拆开看
工厂方法模式通常包含四个角色:
| 角色 | 说明 | 类比 |
|---|---|---|
| Product(抽象产品) | 定义产品对象的公共接口或抽象类 | 菜单上的菜品类目 |
| ConcreteProduct(具体产品) | 实现Product接口的具体类 | 宫保鸡丁、鱼香肉丝、麻婆豆腐 |
| Creator(创建者) | 声明工厂方法,返回Product类型的对象 | 厨房的操作标准/流程 |
| ConcreteCreator(具体创建者) | 实现工厂方法,返回具体的ConcreteProduct实例 | 负责做某道菜的具体厨师 |
创建者在代码里通常是这样的结构:
java复制public abstract class ReportGenerator {
// 业务逻辑:生成报告时会经过一系列编排
public Report generate(String source) {
ReportData data = fetchData(source);
// 这里调用了工厂方法
Report report = createReport(data);
report.validate();
return report;
}
// 工厂方法,交给子类决定具体类型
protected abstract Report createReport(ReportData data);
}
public class PdfReportGenerator extends ReportGenerator {
@Override
protected Report createReport(ReportData data) {
return new PdfReport(data);
}
}
你注意看,generate方法里已经有了一套完整的业务编排,但它没有直接new PdfReport,而是调用createReport。这就是工厂方法模式最迷人的地方——算法骨架在父类固定,具体创建细节由子类注入。以后新增一个Excel报告生成器,你只需要加一个ExcelReportGenerator继承ReportGenerator,老代码一行不用改。
2.3 核心思想:延迟到子类去new
把“new”延迟到子类,带来几个实际好处:
第一,替换产品类型时不需要动调用方。 比如日志框架里,开发环境输出到控制台,生产环境输出到文件,多环境切换只需要在启动时注入不同的LoggerFactory子类。
第二,产品创建逻辑和业务使用逻辑解耦。 业务层面对的是Product接口,根本不需要知道具体的实现类,创建的过程被封装在工厂子类里。
第三,符合开闭原则。 新增产品形态不需要修改已有的Creator和ConcreteCreator,只需要新增类。
但也要泼一盆冷水:工厂方法不是银弹。如果产品本身没有任何变化,或者项目中只有一个具体产品类,硬套工厂方法反而制造了一层没有意义的间接层,类数量翻倍,代码却没有任何收益。我在实际项目中判断要不要用工厂方法时,通常会看一个问题:这个产品类型未来真的会扩展吗? 如果答案是“大概率不会”,那就不需要。
3. 工厂方法的典型场景与代码对照
3.1 场景一:日志记录器
日志系统是工厂方法模式最经典的案例,几乎每个设计模式教程都会提到。假设我们的系统有控制台日志、文件日志、数据库日志,客户端统一调用日志抽象接口:
java复制public interface ILogger {
void log(String message);
}
public class ConsoleLogger implements ILogger {
@Override
public void log(String message) {
System.out.println("Console: " + message);
}
}
public class FileLogger implements ILogger {
@Override
public void log(String message) {
// 写入文件的逻辑
}
}
public abstract class LoggerFactory {
public void writeLog(String message) {
ILogger logger = createLogger();
logger.log(message);
}
protected abstract ILogger createLogger();
}
public class ConsoleLoggerFactory extends LoggerFactory {
@Override
protected ILogger createLogger() {
return new ConsoleLogger();
}
}
这个模式的好处是什么?当业务系统需要记录日志时,它只依赖LoggerFactory这个抽象类,以及ILogger这个接口。将来如果需要把日志发送到消息队列,只需要新增MqLogger和MqLoggerFactory,整个业务系统不用动。在真实项目里,我见过很多团队把日志封装成静态工具类,用if判断环境类型,每换一种日志通道就要改动工具类,这就是简单工厂滥用后带来的维护成本。
3.2 场景二:多个订单解析器
另一个非常适合工厂方法的场景是“不同类型的文件导入解析”。比如一个电商后台需要支持Excel、CSV、XML三种格式的订单批量导入,每种格式的解析规则完全不同:
- Excel解析:按单元格位置读取,需要处理合并单元格、日期格式
- CSV解析:按分隔符切分,要注意引号转义
- XML解析:按节点遍历,需要处理命名空间
如果用if分支去写,OrderImportService会越来越长,而且每种格式的解析逻辑会交叉耦合。采用工厂方法后,解析器本身的代码结构会很干净:
java复制public interface OrderParser {
List<Order> parse(InputStream inputStream);
}
public class ExcelOrderParser implements OrderParser {
@Override
public List<Order> parse(InputStream inputStream) {
// Excel解析逻辑
return null;
}
}
public abstract class OrderParserFactory {
public List<Order> importOrders(InputStream in) {
OrderParser parser = createParser();
return parser.parse(in);
}
protected abstract OrderParser createParser();
}
实际项目中,文件格式往往会伴随版本变化,比如ExcelV1、ExcelV2。这种情况下,工厂方法的价值更加明显:每个文件格式版本对应一个具体的工厂子类,导入逻辑的骨架统一放在父类,版本差异被限制在各自的工厂和解析器里,不会一路传染到业务层。
3.3 实际项目中的落地判断
说了这么多场景,我在极客时间等社群交流时经常被问到:到底什么时候才值得用工厂方法?我给一个比较实用的判断标准。
- 如果你的对象创建逻辑只有一行
new,没有参数差异、没有初始化流程、没有选择逻辑,不需要工厂方法。 - 如果创建某类对象时需要根据配置、环境、版本等条件进行复杂判断,并且这一判断逻辑大概率会扩展,考虑工厂方法。
- 如果多个类实现了同一个接口,但是调用方关心的是接口行为而不是具体类型,同时这些类会持续增加,工厂方法很合适。
- 如果你正在设计一个框架、库,希望使用方可以扩展自定义实现,工厂方法几乎是必选项。
回到代码层面,工厂方法并不是必须要配合抽象类。Java里也可以用接口、函数式接口来实现。比如使用Supplier或者Function作为工厂方法,在Spring里通过@Bean加FactoryBean也是工厂方法的变体。理解“延迟到子类”这个思想,比背结构更重要。
4. 原型模式:定义、结构与核心思想
4.1 从“复制”到“克隆”的正确理解
原型模式的定义是:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
看到“拷贝”两个字,很多开发者第一反应是“拷贝对象不是用构造函数重新创建一下就行吗?也没多慢啊。”这个想法恰恰忽略了原型模式真正的价值。原型模式的意义不在于“省掉一次构造函数”,而在于创建这个对象时节省的整个过程。
举个例子,一个复杂的报表配置对象,里面包含了权限设置、字段映射、样式配置、数据源连接信息,从数据库加载并初始化这样一个对象可能需要几百毫秒。如果每次创建都需要重新加载、重新组装,在高频场景下性能会非常难看。这时候直接原型一份已加载好的对象,开销从几百毫秒变成几毫秒,效率提升是质的飞跃。
4.2 浅拷贝与深拷贝
理解原型模式,绕不开浅拷贝和深拷贝。这也是面试中的高频考点。
浅拷贝:只复制对象自身的基本类型字段和引用字段的引用,不复制引用指向的对象。换句话说,拷贝出来的新对象和原对象共享内部引用对象。
java复制public class ReportTemplate implements Cloneable {
private String name;
private List<String> metrics;
@Override
public ReportTemplate clone() {
try {
return (ReportTemplate) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
上面的代码就是典型的浅拷贝。复制出来的ReportTemplate虽然是一个新对象,但它的metrics列表和原对象是同一个引用。如果你在克隆对象里修改metrics,原对象也会被修改。这是很多人在实际项目中遇到“复制后数据错乱”的根本原因。
深拷贝:不仅复制对象自身,还会递归复制其内部的引用对象,让克隆对象和原对象完全独立。深拷贝的实现方式有三种:
- 重写
clone()方法,手动复制内部对象 - 使用序列化反序列化方式(Java原生序列化、JSON序列化)
- 使用工具库如Apache Commons Lang的
SerializationUtils,或Spring的BeanUtils
从工程实践来说,我不太建议重度依赖Java原生clone()做深拷贝,因为实现起来容易遗漏嵌套字段,而且对集合类型处理繁琐。更推荐用JSON序列化或者专门的对象拷贝工具。虽然序列化性能稍差,但正确性和可维护性更好。
4.3 核心思想:利用已有实例快速生成新对象
原型模式的核心思想可以总结为一句话:把“创建”变成“复制”。
常规的对象创建走的是构造函数,要经历字段初始化、对象状态设置、依赖注入等步骤,有时还要走数据库查询、远程调用。原型模式绕开这整套流程,直接从现有实例出发,通过内存复制快速得到一个状态相同的新实例。这在大对象、高并发、频繁创建的场景下优势尤其明显。
原型模式还有一层容易被忽略的价值:复制时保留了对象的“运行时状态”。比如一个配置对象在程序启动时被加载并完成了一系列复杂计算,这些计算结果就是对象的状态。如果重新走构造函数,这些状态可能还需要重新加载一遍,而原型模式直接交付一个拥有相同状态的副本,省掉的不只是时间,还有重新构建状态的复杂度。
5. 原型模式的典型场景与代码对照
5.1 场景一:报表模板复制
我在实际项目里用过一次原型模式,就是处理“报表模板”的业务。用户需要创建一张新报表时,希望基于上一张报表的样式、指标、权限配置快速生成,新报表再在此基础上调整。
数据结构简化后是这样的:
java复制public class ReportTemplate implements Cloneable {
private Long id;
private String name;
private TemplateStyle style;
private List<MetricConfig> metrics;
// 深拷贝
@Override
public ReportTemplate clone() {
ReportTemplate copy;
try {
copy = (ReportTemplate) super.clone();
// 手动深拷贝可变对象
if (this.style != null) {
copy.style = this.style.clone();
}
if (this.metrics != null) {
copy.metrics = new ArrayList<>();
for (MetricConfig metric : this.metrics) {
copy.metrics.add(metric.clone());
}
}
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
return copy;
}
}
用户点击“基于当前模板创建新报表”时,直接template.clone(),拿到一个和当前配置一模一样的新对象,再修改名称和指标即可。如果没有原型模式,你需要写一堆字段拷贝代码,而且新增一个字段时容易漏掉,调试起来很痛苦。
5.2 场景二:多级缓存中的快照
另一个典型场景是缓存快照。系统需要把某个配置对象在不同时刻的状态做成快照,供后续回滚或对比。如果每次快照都重新查询数据库、重新组装对象,不仅慢,而且很难保证和原始对象完全一致。这时对当前配置对象做一次克隆,拿到的快照就是当前状态的完整复制。
这个场景在规则引擎、工作流引擎、促销活动配置里都很常见。比如双十一促销活动配置,运营人员会频繁修改规则,为了保证发布时不会因为中途修改导致数据不一致,可以在发布前先克隆一份用于版本存档。
5.3 不同语言里的原型实现差异
原型模式在不同语言里的实现方式差别很大,掌握一两种主流即可。
Java:通过Cloneable接口和clone()方法,但需要注意clone()是浅拷贝,深拷贝需要自己处理。
Python:使用copy模块的copy.copy()和copy.deepcopy(),语言级支持非常方便,几乎不需要自己写模板方法。
Go:通过Clone()接口约定,实现时手动为每个结构体编写克隆方法,自由度最大。
JavaScript:Object.create()、Object.assign()、展开运算符都能做浅拷贝,深拷贝通常借助structuredClone或JSON序列化。
这种语言差异告诉我们一个道理:设计模式是思想,不是代码模板。实现手段可以不同,核心思想保持一致即可。
6. 工厂方法和原型的对比与组合使用
6.1 一张表看清差异
| 维度 | 工厂方法模式 | 原型模式 |
|---|---|---|
| 创建方式 | 通过工厂子类决定具体产品类型 | 通过复制已有原型实例创建新对象 |
| 核心关注点 | “该创建哪个类” | “怎样快速复制一个状态相同的对象” |
| 依赖关系 | 依赖抽象产品接口、具体产品类、工厂子类 | 依赖原型类的clone方法 |
| 构造过程 | 走构造函数和相关初始化 | 绕过构造函数,直接内存复制 |
| 性能 | 取决于构造函数和初始化逻辑 | 通常比新建对象快,大对象场景更明显 |
| 扩展性 | 增加新产品需要增加新工厂子类 | 增加新原型类需要实现克隆逻辑 |
| 适合场景 | 类型多变、创建决策复杂 | 大对象、相同状态、频繁创建 |
需要注意的是,原型模式虽然性能优势明显,但它对“对象结构”有隐含要求:对象必须是可克隆的。如果一个对象内部包含文件句柄、数据库连接、线程池等不可复制资源,强行克隆会带来严重问题,这时候就要慎用。
6.2 组合使用的模式链
工厂方法和原型模式并不是互斥关系,实际项目中经常组合使用。最常见的组合是:工厂方法负责根据条件选择具体原型类,原型负责复制出最终对象。
举个例子,一个工作流引擎需要根据流程类型(请假、报销、采购)创建工作流实例。每种类型的流程基础配置相同,但具体节点、审批人不同。这时候可以先用工厂方法根据流程类型获取对应的ProcessPrototype,再调用clone()方法复制出一份可修改的流程实例。这样既利用了工厂方法的类型分派能力,又利用了原型模式的复制效率。
java复制public abstract class ProcessFactory {
public ProcessInstance createProcess(ProcessType type) {
ProcessPrototype prototype = getPrototype(type);
// 深拷贝原型,返回独立的实例
return prototype.clone();
}
protected abstract ProcessPrototype getPrototype(ProcessType type);
}
6.3 选型的三条判断规则
我个人在项目里做选型判断时,会依次问三个问题:
- 当前系统是否需要支持多种产品类型,且这种类型很可能继续扩展? 需要就考虑工厂方法,不需要就放弃。
- 对象创建成本是否高,比如数据库加载、远程调用、复杂计算? 成本高,且对象状态需要保留,优先考虑原型模式。
- 对象结构是否适合浅拷贝或深拷贝? 如果对象图太复杂、循环引用、不可复制资源太多,原型模式的实现成本可能超过收益。
这三条规则不保证绝对正确,但能帮你在设计评审时快速形成判断。很多模式用的不好,不是模式本身有问题,而是没有在正确的场景下使用。
7. 实战中容易踩的坑,以及我的处理方式
7.1 工厂方法模式的过度设计
我见过最典型的错误是:一个项目里只有一个产品实现类,但是开发按“未来扩展”的名义先写了工厂方法。几个月后,产品形态没有任何变化,代码里却多了一堆抽象工厂、具体工厂,每次阅读代码都要多跳两层。
这种过度设计真正的危害不是“类多了几个”,而是把简单的逻辑复杂化了。团队里其他人维护时,需要理解这一层工厂存在的意义,如果找不到必要性,就会产生认知负担。
我的建议是:遵循最小可用设计。等第二个具体产品出现时再引入工厂方法,往往不晚,甚至更合理,因为那时你对产品变体的理解更清晰,抽象也更能抽到点子上。
7.2 原型模式浅拷贝的引用共享
这是原型模式在真实项目里翻车最多的地方。很多开发者写了clone()之后,以为拿到的是完全独立的副本,结果修改列表字段时,原对象也被改了,最终导致脏数据、并发异常等一系列问题。
有一次我在项目中排查一个“报表模板被批量修改后,其他报表样式也跟着变”的线上问题,最后定位到就是浅拷贝导致的引用共享。某些嵌套对象被所有克隆对象共享,一个模板改了样式,其他所有基于它克隆出来的模板全部同步变样。
解决方式有两个方向:
- 如果嵌套对象是不可变对象(String、包装类型等),浅拷贝没有问题。
- 如果是可变对象(集合、自定义类),必须做深拷贝,否则一定出问题。
如果没有把握保证每个嵌套对象都处理干净,最稳妥的方案是直接用序列化方式做深拷贝。虽然性能差一些,但正确性优先。
7.3 原型模式不走构造函数的隐含风险
原型模式的克隆过程是内存层面的复制,不会执行构造函数。这意味着构造函数里的初始化逻辑、校验逻辑、权限校验、事件通知等,在克隆时全部不会触发。
这既是优势也是风险。优势在于省掉了重复初始化,风险在于:如果构造函数里有必须执行的逻辑,比如新增对象时要记录创建日志、要初始化外部依赖、要校验参数合法性,那克隆出的对象会跳过这些环节,可能导致状态不完整。
我的做法是:在clone()方法内部,复制完基本字段后,再手动执行必要的初始化逻辑。不要默认克隆出来的对象是“完整可用”的,要把对象状态校验纳入克隆流程。
曾经有一次,我把一个包含数据库连接池的对象加入了原型模式,结果克隆出来的对象全部共用同一个连接池,导致连接池管理混乱,最终不得不重构。从那以后,我见到内部包含不可复制资源的对象,第一反应就是:不要对它使用原型模式,或者至少要用深拷贝并且单独处理资源字段。
最后再分享一点个人体会
学设计模式这件事,最容易犯的错是把结构背得滚瓜烂熟,但面对真实代码时不知道怎么用。我在带团队做代码评审时,评判一个模式用得好不好,从来不看结构是否标准,而是看它是否真正解决了某个“变化点”或“成本点”。工厂方法解决的是创建决策的变化,原型解决的是创建成本的高企。当你写代码时感觉“创建对象这段很绕”“复制对象这段很啰嗦”,这两个模式才是真正可以拿出来用的工具。
我踩过不少坑之后,现在的习惯是:先写最直观的代码,等到第二个变化点出现再引入工厂方法;先确认对象是否可安全复制,再决定要不要用原型模式。设计模式是“设计”出来的,不是套出来的——这句话希望你能真的听懂。
