上一篇单例管的是"只造一个",这一篇的工厂,管的是另一个问题:当一个东西有很多种、到底该造哪一种? 还记得开篇那坨支付代码里的 if (payType.equals("wechat")) ... else if ("alipay") ... 吗?每接一个新渠道就得回去加一个分支------这正是工厂模式要收拾的烂摊子。
先澄清一个几乎所有初学者都会绕晕的地方:工厂相关的模式其实有三个 ,而且名字很像------简单工厂 、工厂方法 、抽象工厂 。它们是层层递进的关系,不是三选一。简单工厂是最朴素的起点(严格说它不算 GoF 二十三式,但绕不开),工厂方法是本篇的主角,抽象工厂是下一篇的内容。这一篇我们把前两者一次讲透,你会清楚地看到:它们不是三种并列的技巧,而是同一个问题在不同"变化压力"下的三次升级。
这篇文章按这条线索展开:先说清楚"new 一个对象"这个再普通不过的动作,为什么也是一种需要治理的耦合;然后用简单工厂把散落各处的 new 收拢到一个地方,再指出它撞上开闭原则的天花板;接着引出工厂方法 ------它把"决定造哪个"这件事从一个大 if-else 里解放出来,下放给一个个专门的子类工厂;最后讲清楚它的四个角色、到底什么场景才值得上工厂,以及它在 JDK/框架里的真实身影。全程还是那套订单支付的例子。
目录
- [从一坨 if-else 说起:new 也是一种耦合](#从一坨 if-else 说起:new 也是一种耦合)
- [简单工厂:先把 new 收拢到一处](#简单工厂:先把 new 收拢到一处)
- 简单工厂的天花板:它违反了开闭原则
- 工厂方法:把"造什么"下放给子类
- 工厂方法的四个角色与骨架
- 到底什么时候才值得上工厂
- 现实身影,以及和其他模式的界限
一、从一坨 if-else 说起:new 也是一种耦合
我们接着开篇的支付场景往下走。第一篇里已经用开闭原则把支付渠道抽象成了一个 Payment 接口,每个渠道是一个实现:
java
public interface Payment {
void pay(double amount);
}
class WechatPayment implements Payment { public void pay(double a){ /*...*/ } }
class AlipayPayment implements Payment { public void pay(double a){ /*...*/ } }
class UnionPayment implements Payment { public void pay(double a){ /*...*/ } }
抽象是抽好了,但"到底该 new 哪个实现"这件事,还散落在业务代码里。订单服务里大概是这么写的:
java
public class OrderService {
public void pay(String payType, double amount) {
Payment payment;
// 根据类型,亲自 new 出对应的实现
if (payType.equals("wechat")) {
payment = new WechatPayment();
} else if (payType.equals("alipay")) {
payment = new AlipayPayment();
} else if (payType.equals("unionpay")) {
payment = new UnionPayment();
} else {
throw new IllegalArgumentException("不支持的支付方式");
}
payment.pay(amount);
}
}
这段代码有个容易被忽视的问题:OrderService 本来只关心"发起支付"这个业务动作,现在却被迫认识了 WechatPayment、AlipayPayment、UnionPayment 每一个具体实现类。 面向接口编程喊了半天,结果在 new 这一步又把具体类给引进来了------因为 new 关键字后面跟的,永远是一个具体类,而不是接口 。你没法 new 一个接口。
这就带来两个坏处。其一,创建逻辑和业务逻辑搅在了一起 :哪天支付宝的构造需要多传一个参数、或者要先做个初始化,你得回 OrderService 里改。其二,这坨 new 的判断会到处复制 :除了下单,退款、查询、对账......只要哪里需要根据类型拿到 Payment,同样的 if-else 就得再抄一遍,改一处要改很多处。
问题的根源是:"创建一个对象"这件事本身,也是一种会变化、需要被隔离的职责。 而隔离职责的第一步,就是把它从业务里拎出来,交给一个专门的角色------工厂。
二、简单工厂:先把 new 收拢到一处
简单工厂(Simple Factory) 的想法非常朴素:既然到处都在根据类型 new 对象,那就把这段逻辑抽到一个专门的类里,谁要对象找它要,别自己 new。 这个专门的类,就叫工厂。
java
public class PaymentFactory {
// 一个静态方法,输入类型,输出对应的 Payment
public static Payment create(String payType) {
switch (payType) {
case "wechat": return new WechatPayment();
case "alipay": return new AlipayPayment();
case "unionpay": return new UnionPayment();
default:
throw new IllegalArgumentException("不支持的支付方式: " + payType);
}
}
}
于是 OrderService 一下子清爽了:
java
public class OrderService {
public void pay(String payType, double amount) {
Payment payment = PaymentFactory.create(payType); // 不再自己 new
payment.pay(amount);
}
}
变化很明显:OrderService 从此只认识 Payment 接口和 PaymentFactory,不再认识任何一个具体的支付实现类。 想知道微信支付怎么造、支付宝构造要不要传参,那是工厂内部的事,业务侧完全不用管。而且退款、对账那些地方,现在也都统一找 PaymentFactory.create() 拿对象,创建逻辑收敛到了唯一一处------要改构造方式,只改工厂。
这一步的收益是实打实的,所以简单工厂在实际项目里用得非常广,是最接地气的一种"模式"。它把"创建"和"使用"分了家,这正是所有工厂模式的共同内核。
一个命名提醒 :简单工厂也叫"静态工厂方法",因为它常用一个静态方法对外提供。它不属于 GoF 的二十三个正统设计模式------GoF 认为它更像一种编程习惯而非模式。但它是理解后面工厂方法、抽象工厂的必要台阶,所以务必先拿下它。
简单工厂看着已经不错了,但它有一个绕不过去的硬伤,而这个硬伤,恰恰踩在我们开篇立下的那条"总纲"上。
三、简单工厂的天花板:它违反了开闭原则
再回头盯着 PaymentFactory.create() 那个 switch。现在产品说,要新接入一个"数字人民币"支付。你得怎么做?
你必须回到 PaymentFactory 这个类里,撬开 create 方法,加一个 case:
java
case "dcep": return new DcepPayment(); // ← 又得回来改这个方法
看出来了吗?这违反了开闭原则 ------每增加一种产品,都要去修改工厂这段已经写好、已经测试过、正在稳定运行的代码。第一篇讲开闭原则时批过的那个 if-else 毛病,其实并没有消失,只是从 OrderService 里搬到了 PaymentFactory 里。工厂把创建逻辑集中了,这是进步;但"集中"也意味着所有新增产品的修改压力,全压在了这一个 switch 上,它会越长越大,最终变成一个谁都不想碰、一碰就要回归测试所有支付渠道的雷区。
这就是简单工厂的天花板:它解决了"创建逻辑散落"的问题,却没解决"新增产品要改代码"的问题。 对于渠道基本稳定、极少新增的场景,简单工厂完全够用,别过度设计;但如果这个产品维度会频繁扩展,我们就需要一种能让"新增产品时不改任何旧代码"的结构。
顺着开闭原则的老思路想:要应对"新增"这种变化,就得把"变化的部分"做成可扩展的抽象。 简单工厂的问题在于,它把"决定造哪个产品"的逻辑,硬编码成了一个集中的 switch。那能不能把这个"决定权"本身也抽象出去、下放出去?这就是工厂方法的核心思路。
四、工厂方法:把"造什么"下放给子类
工厂方法模式(Factory Method) 的定义是:定义一个用于创建对象的接口(工厂接口),但把"到底实例化哪个类"这件事,推迟到子类去决定。 一句话------不再用一个大工厂配一个大 switch,而是每种产品配一个自己的小工厂。
具体怎么做?先把"工厂"也抽象成一个接口:
java
// 抽象工厂接口:只声明"我能造一个 Payment",但不说造哪个
public interface PaymentFactory {
Payment create();
}
然后,每一种支付渠道,都对应一个自己的工厂实现类,各自只负责造自己那一种产品:
java
public class WechatPaymentFactory implements PaymentFactory {
public Payment create() { return new WechatPayment(); }
}
public class AlipayPaymentFactory implements PaymentFactory {
public Payment create() { return new AlipayPayment(); }
}
public class UnionPaymentFactory implements PaymentFactory {
public Payment create() { return new UnionPayment(); }
}
OrderService 现在依赖的是工厂接口,由外部把具体用哪个工厂注入进来(这正是第一篇讲的依赖倒置):
java
public class OrderService {
private final PaymentFactory factory; // 只依赖工厂接口
public OrderService(PaymentFactory factory) { this.factory = factory; }
public void pay(double amount) {
Payment payment = factory.create(); // 造哪个,由注入的工厂决定
payment.pay(amount);
}
}
// 用的时候,决定注入哪个工厂
new OrderService(new WechatPaymentFactory()); // 这个订单走微信
new OrderService(new AlipayPaymentFactory()); // 那个订单走支付宝
关键的变化在这里:现在要新增"数字人民币",你要做的是------新建一个 DcepPayment 类,再新建一个 DcepPaymentFactory 类,然后就完了。 你没有修改任何一行已有的代码,PaymentFactory 接口没动、其他三个工厂没动、OrderService 没动。这就真正做到了"对扩展开放、对修改关闭"。
对比一下就看清了这次升级的本质:
- 简单工厂 :一个工厂类,内部一个
switch决定造什么。新增产品 → 改 那个 switch(违反开闭)。 - 工厂方法 :一个工厂接口,每个产品配一个工厂子类。新增产品 → 加 一个产品类 + 一个工厂类(符合开闭)。
代价当然也有:类的数量变多了------每来一个产品,就多一个配套的工厂类。这是工厂方法的固有成本,后面第六节会专门说这笔账划不划算。
五、工厂方法的四个角色与骨架
把上面的代码抽象一下,工厂方法模式里其实有四个固定角色,认准它们,你在任何代码里都能一眼认出这个模式:
| 角色 | 本例中是谁 | 职责 |
|---|---|---|
| 抽象产品(Product) | Payment 接口 |
定义产品的公共行为 |
| 具体产品(Concrete Product) | WechatPayment 等 |
产品的具体实现,是被创建的目标 |
| 抽象工厂(Creator) | PaymentFactory 接口 |
声明工厂方法 create(),返回抽象产品 |
| 具体工厂(Concrete Creator) | WechatPaymentFactory 等 |
实现工厂方法,决定造哪个具体产品 |
它们的关系是两条平行的继承体系 :一边是产品家族(Payment → 各种 Payment),一边是工厂家族(PaymentFactory → 各种 Factory),而且一个具体工厂,精确对应一个具体产品。用一张图看这个"平行结构"最清楚:

这张图里最该记住的,就是那条"一一对应、并排扩展 "的关系:产品和工厂成对出现,新增时并排加一对新的,老的纹丝不动。这也解释了工厂方法名字的由来------那个 create() 就是所谓的"工厂方法",它被声明在抽象层、实现在子类,把"实例化哪个类"的决策权,从调用方转移给了子类。
有一种常见变体值得一提:抽象工厂(Creator)不一定是纯接口,也可以是个抽象类 ,里面除了抽象的工厂方法,还能放一些所有工厂共用的逻辑。比如工厂方法造出产品后,统一记一条日志、做一次校验------这些公共动作写在抽象类里,子类只负责实现那个真正
new对象的工厂方法。这种"模板 + 工厂方法"的组合在框架里很常见。
六、到底什么时候才值得上工厂
讲到这儿,必须泼一盆和开篇一脉相承的冷水:工厂方法不是越用越好,它是有成本的,滥用就是过度设计。
我们把这三层的适用场景摊开说清楚,你才好对号入座:
- 什么都不做(直接 new) :如果一个对象就一种实现、创建也简单,那就老老实实
new,别整任何工厂。给一个只有一种实现的类套上工厂接口,是最典型的过度设计,除了让读代码的人多跳转几次,毫无收益。 - 简单工厂 :当同一个接口有多种实现 ,且创建逻辑想收拢到一处、避免业务里到处
new时,用简单工厂。它最轻量,代价只是"新增产品要改 switch"。如果你的产品种类基本稳定、极少新增(比如支付渠道其实一年也加不了一个),简单工厂就是性价比最高的选择,别硬上工厂方法。 - 工厂方法 :当产品维度会频繁扩展 、你希望新增产品时完全不碰旧代码,或者创建过程本身比较复杂(要组装、要配置、要依赖别的对象),值得为每个产品配一个工厂时,才升级到工厂方法。它用"类数量增多"换来了"完美的开闭"。
一句话决策:先问这个对象值不值得用工厂(有没有多实现),再问值不值得用工厂方法(会不会频繁新增)。 两个问题都为"是",工厂方法才划算;第一个就为"否",连简单工厂都不用上。
这里再强调一遍第一篇的判断力问题:模式是用来解决真实存在的变化压力的。你得先看到"这个维度确实在不停加新东西"这个事实,才谈得上用工厂方法去接住它。为想象中根本不会发生的扩展预先铺一堆工厂类,和不用模式导致代码烂,是同样糟糕的两种极端。
七、现实身影,以及和其他模式的界限
工厂方法在 JDK 和主流框架里遍地都是,认出它们能帮你巩固理解:
Collection.iterator():这是教科书级的工厂方法。Collection是抽象工厂(Creator),iterator()是工厂方法,Iterator是抽象产品。ArrayList、LinkedList各自实现iterator(),返回自己专属的迭代器实现------每个具体集合(具体工厂)造出自己专属的迭代器(具体产品),严丝合缝地对应工厂方法的结构。java.util.Calendar.getInstance():根据地区、时区返回不同的Calendar子类实例,是简单工厂(静态工厂方法)的典型。- 日志框架
LoggerFactory.getLogger()、Spring 的BeanFactory:名字里就带 Factory,思想都是"把对象的创建交给专门的工厂,使用方只面向抽象"。其中 Spring 的BeanFactory是整个容器的基石,它把"创建和管理 Bean"这件事做到了极致。
最后划清两条容易混淆的界限:
- 工厂方法 vs 抽象工厂(下一篇) :工厂方法的一个工厂只造一种 产品(一个
create());抽象工厂的一个工厂要造一整族、多种 互相关联的产品(比如同时造Order+Invoice+Shipment)。可以先记一句:工厂方法管"一个产品的多种实现",抽象工厂管"一族产品的整套搭配"。 下一篇细讲。 - 工厂方法 vs 建造者(第 5 篇) :两者都在管"对象创建",但关注点不同。工厂方法关心的是"造哪一个 "(在多个类型里选一个);建造者关心的是"怎么一步步造好一个"(一个复杂对象的分步装配)。前者的产品通常是不同的类,后者通常是同一个类的复杂实例。
小结 。这一篇讲的是"创建对象"这件事该如何被治理。核心是一条清晰的演进线:直接 new 把具体类耦合进了业务;简单工厂 把创建收拢到一处、让业务只面向接口,但新增产品仍要改那个 switch,撞上了开闭原则的天花板;工厂方法 再进一步,把"决定造哪个"的决策权下放给一个个工厂子类,做到了新增产品时完全不改旧代码。记住那张"产品与工厂平行、成对扩展"的结构图,也记住那句冷水------先确认真有多实现、真会频繁扩展,工厂方法才值得上,否则简单工厂甚至直接 new 就够了 。下一篇我们把工厂再往上抬一层:当你要造的不是一个产品,而是一整族互相搭配的产品时,就轮到抽象工厂登场了。