☰
Java简单工厂模式:从核心痛点到实战避坑的完整指南
2026/11/4 5:56:22 网站建设 项目流程

工厂模式大概是所有设计模式里最容易被"用歪"的一个。我见过太多项目,代码里到处是XxxFactory.getXxx(type),点进去一看,一个巨大的switch里塞了二十几个分支,每加一个类型就得改这个类,改完还得重新测一遍所有分支——这哪是工厂模式,分明是把if-else换了个马甲。简单工厂(Simple Factory)本身并不复杂,但恰恰因为它简单,很多人只学了"有个工厂类负责 new 对象"这个皮毛,没搞清楚它到底解决了什么问题、边界在哪里、什么时候该用它、什么时候该果断放弃它。这篇内容就围绕 Java 简单工厂模式展开,从它要解决的核心痛点讲起,把完整实现拆开揉碎,再聊聊我在实际项目里踩过的坑和总结出的取舍经验。不管你是刚接触设计模式的新手,还是写过一堆工厂类却总觉得别扭的老手,应该都能从里面找到点有用的东西。

1. 先搞清楚简单工厂到底在解决什么问题

1.1 从一段"能跑但不敢改"的代码说起

假设你在做一个订单系统,需要支持多种支付方式。最直接的写法是在业务方法里根据类型判断,然后 new 对应的支付对象:

public class OrderService { public void pay(String payType, BigDecimal amount) { if ("alipay".equals(payType)) { AlipayPayment payment = new AlipayPayment(); payment.doPay(amount); } else if ("wechat".equals(payType)) { WechatPayment payment = new WechatPayment(); payment.doPay(amount); } else if ("unionpay".equals(payType)) { UnionPayPayment payment = new UnionPayPayment(); payment.doPay(amount); } // 每加一种支付方式,这里就多一个分支 } }

这段代码能跑,逻辑也没错,但它有个致命问题:对象的创建逻辑和使用逻辑耦合在一起。OrderService本来只该关心"发起支付"这件事,结果它还得知道每种支付类叫什么、怎么实例化。一旦支付方式增加到十几种,这个方法会膨胀得没法看,而且每次新增都要改动这个已经稳定的类,违反了开闭原则的精神。

简单工厂要做的,就是把这坨创建逻辑抽出去,让使用方只跟一个统一的入口打交道。它的核心思想可以用一句话概括:把"根据条件创建不同对象"这件事,收敛到一个专门的类里,调用方只传参数、拿结果,不关心 new 的过程。

1.2 简单工厂的三方角色划分

理解简单工厂,关键是理清它涉及的三方角色,这三方各司其职,边界清晰:

角色职责典型形态
产品接口/抽象类定义所有产品共有的行为契约Payment接口
具体产品实现接口,提供各自的具体逻辑AlipayPayment、WechatPayment
工厂类根据传入参数决定创建哪个具体产品PaymentFactory

调用方(客户端)只依赖产品接口和工厂类,完全不接触具体产品类。这个依赖关系的转变,是简单工厂价值的根源——它把"编译期依赖具体类"变成了"运行期根据参数决定具体类"。

这里有个容易被忽略的点:简单工厂返回的必须是接口或抽象类,而不是具体类型。如果工厂方法返回的是AlipayPayment这种具体类,那调用方还是得知道具体类型,抽象就白做了。我见过有人写工厂方法返回具体类型,然后调用方拿到之后还要强转,这种写法等于把简单工厂的意义完全消解掉了。

1.3 为什么它叫"简单"工厂,以及它不属于 GoF 23 种模式

有个常被问到的问题:简单工厂算不算设计模式?严格来说,GoF 的 23 种设计模式里并没有"简单工厂",它更像是一种编程习惯或过渡性方案。工厂方法模式(Factory Method)和抽象工厂模式(Abstract Factory)才是正式成员。

那为什么它还被广泛使用?因为它的门槛极低、收益直接。在很多场景下,你不需要工厂方法那种"每个产品配一个工厂"的复杂度,也不需要抽象工厂那种"产品族"的宏大结构,你只是想把一段创建逻辑收拢起来,简单工厂就够了。把它理解成"工厂方法模式的简化版"或者"创建逻辑的封装手法"更准确。

提示:面试时如果被问"简单工厂是不是设计模式",比较稳妥的回答是:它不属于 GoF 23 种,但在工程实践中被广泛采用,可以看作工厂方法模式的一种简化形态。

2. 完整实现拆解:从接口到工厂的每一行代码

2.1 产品接口的设计要点

先定义产品接口。这一步看似简单,但接口方法的设计直接决定了后续扩展的难易度:

public interface Payment { /** * 执行支付 * @param amount 支付金额 * @return 支付结果 */ PayResult doPay(BigDecimal amount); }

设计产品接口时有几个实操层面的考量。第一,方法参数尽量用抽象类型,比如金额用BigDecimal而不是double,避免精度问题;第二,返回值要能承载失败信息,别用void,否则调用方无法判断支付是否成功;第三,接口方法数量要克制,简单工厂的产品接口通常只放最核心的一两个方法,方法太多会导致每个具体产品都要实现一堆用不上的逻辑。

我个人的经验是,产品接口的粒度宁粗勿细。曾经有个项目,产品接口里塞了七八个方法,结果一半的具体实现类里都是空方法或者抛UnsupportedOperationException,这种"胖接口"是设计上的坏味道。如果确实需要区分能力,宁可拆成多个接口,让具体产品按需实现。

2.2 具体产品类的实现

具体产品类各自实现接口,互不干扰:

public class AlipayPayment implements Payment { @Override public PayResult doPay(BigDecimal amount) { // 调用支付宝相关逻辑 System.out.println("支付宝支付: " + amount); return PayResult.success("ALIPAY-" + System.currentTimeMillis()); } } public class WechatPayment implements Payment { @Override public PayResult doPay(BigDecimal amount) { System.out.println("微信支付: " + amount); return PayResult.success("WECHAT-" + System.currentTimeMillis()); } }

具体产品类里要注意的是别把创建时的初始化逻辑写得太重。有些产品对象创建时需要读配置、建连接,如果这些操作放在构造函数里,工厂每次创建都会触发,性能会很差。这种场景下要么用对象池,要么把重量级初始化延迟到真正使用时(懒加载)。简单工厂本身不解决对象复用问题,它只负责"创建",复用是另一回事。

2.3 工厂类的核心实现与参数设计

工厂类是重头戏。最基础的实现就是一个静态方法加条件判断:

public class PaymentFactory { public static Payment create(String payType) { if ("alipay".equals(payType)) { return new AlipayPayment(); } else if ("wechat".equals(payType)) { return new WechatPayment(); } else if ("unionpay".equals(payType)) { return new UnionPayPayment(); } throw new IllegalArgumentException("不支持的支付类型: " + payType); } }

这段代码有几个值得说的细节。第一,参数用字符串还是枚举?字符串灵活但容易拼错,枚举安全但扩展时要改枚举定义。我的建议是:如果类型集合相对稳定,用枚举;如果类型来自外部配置或数据库,用字符串并在工厂内部做校验。第二,找不到匹配时怎么办?千万别返回null,那会让调用方在不知情的情况下触发空指针。抛异常或者返回一个"默认产品"(比如空对象模式)都比返回 null 强。第三,方法用 static 还是实例方法?静态方法调用方便,但不利于测试时替换(没法 mock)。如果工厂内部逻辑复杂、需要依赖其他组件,用实例方法配合依赖注入更合适。

2.4 用 Map 注册表替代 if-else 的进阶写法

当产品类型多起来,if-else链会变得难以维护。这时候可以用一个 Map 把"类型标识"和"创建逻辑"映射起来:

public class PaymentFactory { private static final Map<String, Supplier<Payment>> REGISTRY = new HashMap<>(); static { REGISTRY.put("alipay", AlipayPayment::new); REGISTRY.put("wechat", WechatPayment::new); REGISTRY.put("unionpay", UnionPayPayment::new); } public static Payment create(String payType) { Supplier<Payment> supplier = REGISTRY.get(payType); if (supplier == null) { throw new IllegalArgumentException("不支持的支付类型: " + payType); } return supplier.get(); } }

这种写法的好处是新增产品只需在静态块里加一行注册,工厂的create方法本身不用动。用Supplier<Payment>而不是直接存实例,是为了保证每次调用都返回新对象(如果产品是无状态的,也可以直接存单例实例,省去重复创建的开销)。

不过这种写法也有代价:注册逻辑集中在静态块里,如果注册项很多,静态块会很长;而且静态初始化在类加载时执行,如果某个产品的构造依赖运行期才有的配置,静态块里就没法处理。所以它适合"产品类型固定、创建逻辑轻量"的场景,不适合需要动态注册的复杂系统。

2.5 一个完整的可运行示例

把上面的部分拼起来,配一个简单的PayResult和测试入口,就是一个能直接跑的完整例子:

public class PayResult { private final boolean success; private final String tradeNo; private PayResult(boolean success, String tradeNo) { this.success = success; this.tradeNo = tradeNo; } public static PayResult success(String tradeNo) { return new PayResult(true, tradeNo); } public static PayResult fail() { return new PayResult(false, null); } @Override public String toString() { return "PayResult{success=" + success + ", tradeNo='" + tradeNo + "'}"; } } public class Demo { public static void main(String[] args) { Payment payment = PaymentFactory.create("alipay"); PayResult result = payment.doPay(new BigDecimal("99.00")); System.out.println(result); } }

跑起来输出支付宝支付: 99.00和对应的PayResult。这个例子虽然简单,但结构是完整的:接口定义契约、具体产品实现逻辑、工厂负责创建、调用方只依赖接口。你可以把它当成一个模板,换成任何"多实现、按类型选择"的场景,比如消息发送(短信/邮件/站内信)、文件解析(CSV/Excel/JSON)、日志输出(控制台/文件/远程)等等。

3. 简单工厂的适用边界:什么时候该用,什么时候别硬套

3.1 它真正擅长的三类场景

简单工厂不是万能的,但在以下几类场景里它确实好用:

第一类,产品类型有限且相对稳定。比如一个系统只支持三种登录方式,短期内不会频繁增加。这种情况下工厂类不会频繁改动,维护成本很低。

第二类,创建逻辑有一定复杂度,需要集中管理。比如创建对象时要读配置、做参数校验、设置默认值,这些逻辑如果散落在各处会重复且容易出错,收拢到工厂里统一处理更可靠。

第三类,调用方不需要关心具体类型。调用方只想要一个"能干活的产品",具体是哪个实现由参数决定。这种"面向接口编程"的场景正是简单工厂的主场。

反过来,如果产品类型会频繁增加、或者创建逻辑极其简单(就是无参 new),那用简单工厂反而是过度设计。我见过有人给一个只有单一实现的接口也套了个工厂,纯属给自己找事。

3.2 违反开闭原则这件事,得辩证看

几乎所有讲简单工厂的文章都会提一句"它违反开闭原则"——新增产品要改工厂类。这话没错,但要看怎么理解。

开闭原则的核心是"对扩展开放、对修改关闭",简单工厂在新增产品时确实要修改工厂类,这是它的固有缺陷。但违反开闭原则不等于不能用。开闭原则是一种理想状态,实际项目中完全遵守它的代价往往很高。如果产品类型一年才增加一两次,为了这点扩展性去搞一套工厂方法模式(每个产品配一个工厂类),代码量翻倍,收益却微乎其微,这笔账不划算。

我的判断标准是:看变更频率和变更影响面。如果新增产品的频率低、且工厂类改动只影响创建逻辑本身(不影响已有产品的行为),那简单工厂完全可以接受。只有当产品类型频繁增加、且每次改动都带来较大回归测试成本时,才值得升级到工厂方法模式。

3.3 和工厂方法、抽象工厂的取舍对照

为了把边界说清楚,做个横向对比:

维度简单工厂工厂方法抽象工厂
结构复杂度低(一个工厂类)中(每产品一个工厂)高(产品族+多工厂)
新增产品改工厂类加新工厂类,不改旧代码改抽象工厂接口,影响大
开闭原则违反遵守部分遵守
适用场景产品少、稳定产品多、需扩展多产品族、需成套创建

简单工厂是"一个工厂管所有产品",工厂方法是"一个产品一个工厂",抽象工厂是"一个工厂管一族产品"。选择哪个,取决于你的产品数量和扩展预期,不是越复杂越好。很多中小项目用简单工厂就绰绰有余,硬上抽象工厂只会让代码变得难以理解。

4. 实战中踩过的坑与优化经验

4.1 参数校验缺失导致的隐蔽 bug

最常见的坑是工厂方法对参数不做校验。调用方传了个null或者空字符串,工厂内部的条件判断全部落空,最后要么返回 null(然后调用方空指针),要么抛出一个语焉不详的异常。我在一个项目里就遇到过:上游传了个拼错的支付类型,工厂返回 null,结果在很远的地方才爆出空指针,排查了半天才定位到源头。

解决办法很简单:工厂方法入口处做严格校验,非法参数立即抛出带明确信息的异常。异常信息里要包含传入的原始参数值,方便排查。另外,如果参数类型是字符串,建议在工厂内部统一做 trim 和大小写归一化,避免因为空格或大小写差异导致匹配失败。

4.2 工厂类膨胀后的拆分策略

当工厂里的产品越来越多,单个工厂类会变得臃肿。这时候可以考虑按业务维度拆分工厂,比如PaymentFactory、MessageFactory、ParserFactory各管一摊,而不是搞一个万能工厂。万能工厂的问题是职责不清、依赖混乱,任何产品变动都要动它,成了整个系统的"改动热点"。

拆分的粒度以"业务内聚"为准:同一业务领域的产品放一个工厂,跨领域的坚决分开。如果拆分后单个工厂还是很大,可以进一步用前面说的 Map 注册表方式,把注册逻辑分散到各个产品类自己的静态块里(每个产品类负责把自己注册进工厂),这样工厂类本身就能保持精简。

4.3 线程安全与对象复用的考量

简单工厂默认每次调用都 new 一个新对象,这在多数场景下没问题。但如果产品对象创建开销大(比如要建连接池),或者产品本身是无状态的、可以复用,那就需要考虑对象缓存。

一个简单的做法是在工厂内部维护一个ConcurrentHashMap缓存已创建的对象:

private static final Map<String, Payment> CACHE = new ConcurrentHashMap<>(); public static Payment create(String payType) { return CACHE.computeIfAbsent(payType, PaymentFactory::doCreate); }

用computeIfAbsent能保证并发下的原子性,避免重复创建。但要注意:只有无状态的产品才能这样缓存。如果产品对象持有请求相关的状态(比如当前用户、当前订单),缓存会导致状态串号,这种场景必须每次创建新对象。判断标准很简单:产品对象里有没有可变的成员变量,有就不能缓存。

4.4 用反射或 SPI 进一步解耦的探索

如果连"在工厂里注册产品"这件事都想省掉,可以用反射根据类名动态创建,或者用 Java 的 SPI 机制让产品自己声明。反射的写法大致是:

public static Payment create(String className) { try { Class<?> clazz = Class.forName(className); return (Payment) clazz.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new IllegalArgumentException("创建失败: " + className, e); } }

这种写法把"类型到类"的映射交给了调用方(传全限定类名),工厂彻底不用改。但代价是失去了编译期检查,类名写错了要到运行时才发现,而且反射有一定性能开销。SPI 机制(META-INF/services声明)则更适合插件式扩展,但配置相对繁琐。

我的建议是:反射和 SPI 属于"产品类型完全动态、无法预知"时的方案,普通业务场景没必要用。它们带来的灵活性和引入的复杂度、可维护性问题需要仔细权衡,多数情况下 Map 注册表已经够用了。

5. 把简单工厂用出价值的几个实操心得

5.1 命名与包结构上的约定

工厂类的命名建议统一用XxxFactory,方法名用create或getInstance,让人一眼能看出这是创建入口。包结构上,把接口、具体产品、工厂放在同一个业务包下(比如com.example.payment),产品实现多的时候再分子包(impl),保持内聚。

有个小细节值得注意:工厂类尽量别暴露具体产品的构造入口。如果具体产品类是 public 的,调用方完全可以绕过工厂直接 new,那工厂的约束力就弱了。可以考虑把具体产品类设为包级私有(不加 public),只通过工厂对外暴露接口,从物理上强制"必须走工厂"。当然,这要求工厂和产品在同一个包下,是一种用访问控制强化设计意图的手法。

5.2 配合枚举让类型更安全

前面提过字符串参数的隐患,用枚举能有效缓解:

public enum PayType { ALIPAY, WECHAT, UNIONPAY } public static Payment create(PayType type) { switch (type) { case ALIPAY: return new AlipayPayment(); case WECHAT: return new WechatPayment(); case UNIONPAY: return new UnionPayPayment(); default: throw new IllegalArgumentException("未知类型: " + type); } }

用枚举的好处是编译期就能发现拼写错误,而且 switch 对枚举的匹配是穷尽的(配合 IDE 提示),新增枚举值时编译器会提醒你补分支。缺点是扩展枚举要改枚举定义,如果类型来自外部(比如数据库配置的字符串),还得做一层字符串到枚举的转换。这个转换建议放在工厂入口统一处理,转换失败时给出清晰提示。

5.3 单元测试该怎么写

简单工厂的测试重点有两个:一是验证每种类型都能创建出正确的产品,二是验证非法类型能被正确拒绝。前者用参数化测试最合适:

@ParameterizedTest @CsvSource({ "alipay, com.example.AlipayPayment", "wechat, com.example.WechatPayment" }) void shouldCreateCorrectProduct(String type, String expectedClass) { Payment payment = PaymentFactory.create(type); assertEquals(expectedClass, payment.getClass().getName()); } @Test void shouldThrowWhenTypeInvalid() { assertThrows(IllegalArgumentException.class, () -> PaymentFactory.create("unknown")); }

如果工厂用了静态方法,测试时没法 mock,只能测真实创建逻辑,这通常也够用。但如果工厂内部依赖了外部配置或服务,静态方法就成了测试障碍,这时候就该考虑改成实例方法 + 依赖注入,把依赖从构造函数传进来,测试时注入 mock 对象。

5.4 从简单工厂平滑演进到工厂方法

项目初期用简单工厂,后期产品类型爆炸式增长,需要升级到工厂方法模式时,怎么平滑过渡?我的做法是保留工厂类的对外接口不变,内部把创建逻辑委托给各个具体工厂:

public class PaymentFactory { private static final Map<String, PaymentCreator> CREATORS = Map.of( "alipay", new AlipayPaymentCreator(), "wechat", new WechatPaymentCreator() ); public static Payment create(String type) { PaymentCreator creator = CREATORS.get(type); if (creator == null) throw new IllegalArgumentException("未知类型: " + type); return creator.create(); } }

这样调用方的代码一行都不用改,工厂内部却已经变成了"每个产品一个创建器"的工厂方法结构。这种渐进式重构的好处是风险可控,每一步都能单独测试,不会因为一次性大改引入难以定位的问题。演进过程中如果发现某个产品的创建逻辑特别复杂,还可以单独给它加缓存或特殊处理,互不影响。

简单工厂这个模式,说它简单是真简单,一个类一个方法就能搞定;但要用好它,得清楚它的能力边界、知道它什么时候会成为负担、明白怎么在合适的时机演进到更复杂的形态。工具本身没有高下,关键是匹配场景。我个人的体会是:先用最简单的方案把问题解决,等复杂度真的上来了再重构,而不是一开始就按最复杂的形态设计。毕竟代码是给人看的,能让人一眼看懂的简单工厂,往往比结构精巧但绕来绕去的复杂模式更有生命力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询