大白话说Java设计模式-04-工厂方法模式(业务实战篇):大白商城支付中心的"造物主"
📌 一句话本质:工厂方法模式就是"想造啥跟我说,我专门有个车间给你造"。
🏷️ 标签:工厂方法模式 / Java 设计模式 / Spring / 支付中心 / 大白商城 🎯 适合:初中级后端 / 想搞懂多态真实用法的工程师
目录
- 一、业务场景引入:大白商城为什么需要"造物主"?
- [二、反面教材:if-else 堆出来的"假工厂"](#二、反面教材:if-else 堆出来的"假工厂")
- [三、模式原理:工厂方法的"两件套 + 一张图"](#三、模式原理:工厂方法的"两件套 + 一张图")
- 四、实战代码:大白商城支付中心完整实现
- [五、工厂方法 vs 抽象工厂:到底有啥区别?](#五、工厂方法 vs 抽象工厂:到底有啥区别?)
- [六、Spring 的 BeanFactory:工厂方法的"祖师爷"](#六、Spring 的 BeanFactory:工厂方法的"祖师爷")
- [七、工程决策 Checklist:什么时候用、什么时候别用](#七、工程决策 Checklist:什么时候用、什么时候别用)
- 八、与其他模式协作
- [九、本篇小结 + 下篇预告](#九、本篇小结 + 下篇预告)
一、业务场景引入:大白商城为什么需要"造物主"?
大白商城 2024 年 6 月搞了一次"接入数字人民币支付"的项目,老板拍板:"一个月内必须上线!" 我作为后端负责人,第一反应不是"这个功能多复杂",而是------
"支付中心能撑住吗?"
打开代码一看,心里一凉。支付中心长这样:
java
public class PaymentService {
public PaymentResult pay(String channel, BigDecimal amount) {
if ("alipay".equals(channel)) {
// 50 行支付宝支付逻辑
return alipayPay(amount);
} else if ("wechat".equals(channel)) {
// 50 行微信支付逻辑
return wechatPay(amount);
} else if ("unionpay".equals(channel)) {
// 50 行银联支付逻辑
return unionPay(amount);
} else if ("digitalRMB".equals(channel)) {
// 数字人民币 - 新增
return digitalRMBPay(amount);
}
throw new IllegalArgumentException("未知支付渠道: " + channel);
}
}
这就是典型的"坏味道":
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 每加一个支付渠道就要改 PaymentService |
改 1 处牵动全身,违反开闭原则 |
| ② | 支付逻辑全部塞在 if-else 里 |
单元测试难写,Mock 不掉 |
| ③ | 渠道之间的差异代码混在一起 | 50 行的支付宝代码和 50 行的微信代码没有任何隔离 |
| ④ | 新人接手看代码要"考古" | 没人知道每个分支干了啥 |
老板要的"一个月上线数字人民币" ,按这种写法要改 7 个文件、回归测试 3 天、QA 测 2 天。算下来根本来不及。
那么问题来了:怎么让支付中心变成"加新渠道不改老代码"?
答案就是------工厂方法模式。
1.1 大白话讲透工厂方法
不讲 UML,不讲定义。咱们继续打比方:
场景:大白商城想造一批"支付机器人",每个机器人只负责一种支付(支付宝/微信/银联)。
- 错误做法 :把所有机器人的造法写在
PaymentService里,加一个新机器人就要改PaymentService。- 正确做法 :开一个"造机器人车间"(PaymentFactory),车间里有专门的"造支付宝机器人的小组"(AlipayClientFactory)、"造微信机器人的小组"(WechatClientFactory)。每个小组只造自己那一种。要新加渠道?建新小组,老车间不动。
工厂方法模式 = 定义一个"造物"的接口(PaymentFactory),让子类决定具体造啥(AlipayClientFactory 造 AlipayClient,WechatClientFactory 造 WechatClient)。
这就是大名鼎鼎的**"开闭原则"**------对扩展开放,对修改关闭。
1.2 工厂方法模式的 3 个真实场景
大白商城里,工厂方法的用武之地远不止支付中心:
| 场景 | "造物主" | 造的"物" |
|---|---|---|
| 支付中心 | PaymentFactory | 支付宝/微信/银联客户端 |
| 物流中心 | LogisticsFactory | 顺丰/中通/京东物流客户端 |
| 短信服务 | SmsFactory | 阿里云/腾讯云/华为云短信客户端 |
| 消息队列 | MqFactory | RocketMQ / Kafka 客户端 |
| 缓存服务 | CacheFactory | Redis / Memcached 客户端 |
| 文件存储 | StorageFactory | 阿里云 OSS / 腾讯云 COS / MinIO 客户端 |
任何"按类型创建不同对象"的场景,工厂方法都是首选。
二、反面教材:if-else 堆出来的"假工厂"
很多新手会说:"这不就是把 if-else 换个写法吗?" ------ 完全不一样。我们先看 4 个反面教材,看看它们会怎么一步步崩。
2.1 反面教材 v1:朴素 if-else(最常见的"反模式")
java
/**
* ❌ 反面教材 v1:朴素 if-else
* 每加一个渠道就要改这里
*/
@Service
public class PaymentServiceV1 {
public PaymentResult pay(String channel, BigDecimal amount) {
if ("alipay".equals(channel)) {
// 假设这是 50 行支付宝逻辑
return new AlipayClient("app_id_xxx").pay(amount);
} else if ("wechat".equals(channel)) {
return new WechatClient("app_id_yyy").pay(amount);
} else if ("unionpay".equals(channel)) {
return new UnionPayClient("app_id_zzz").pay(amount);
} else {
throw new IllegalArgumentException("未知渠道: " + channel);
}
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 每加一个渠道改这里 | 改 1 处牵动支付中心所有逻辑,回归测试 3 天 |
| ② | 无法独立测试 | Mock 渠道只能在 if-else 里打桩 |
| ③ | 职责不清 | 支付服务"既管支付又管创建" |
| ④ | 新人难接手 | 50 行的支付宝代码和 50 行的微信代码混在一起 |
2.2 反面教材 v2:switch 也不好使
java
/**
* ❌ 反面教材 v2:switch 写法和 if-else 等价
* 换汤不换药
*/
public PaymentResult pay(String channel, BigDecimal amount) {
return switch (channel) {
case "alipay" -> new AlipayClient("app_id_xxx").pay(amount);
case "wechat" -> new WechatClient("app_id_yyy").pay(amount);
case "unionpay" -> new UnionPayClient("app_id_zzz").pay(amount);
default -> throw new IllegalArgumentException("未知渠道: " + channel);
};
}
问题 :switch 和 if-else 是等价的,没有本质区别。换语法不换结构。
2.3 反面教材 v3:用反射看似"灵活"?
java
/**
* ❌ 反面教材 v3:反射创建
* 看似不用改老代码,实则挖了三个坑
*/
public class PaymentServiceV3 {
private static final Map<String, String> CHANNEL_MAP = new HashMap<>();
static {
CHANNEL_MAP.put("alipay", "com.dabai.mall.payment.AlipayClient");
CHANNEL_MAP.put("wechat", "com.dabai.mall.payment.WechatClient");
}
public PaymentResult pay(String channel, BigDecimal amount) throws Exception {
String className = CHANNEL_MAP.get(channel);
if (className == null) {
throw new IllegalArgumentException("未知渠道: " + channel);
}
// 反射创建
Class<?> clazz = Class.forName(className);
PaymentClient client = (PaymentClient) clazz.getDeclaredConstructor().newInstance();
return client.pay(amount);
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 编译期类型检查失效 | 类名写错、类不存在,运行时才崩 |
| ② | 性能差 | 每次调用都要走反射,吞吐量打 5 折 |
| ③ | 可读性极差 | 谁看得懂 CHANNEL_MAP.put("alipay", "com.dabai...")? |
| ④ | 构造参数无法传递 | 想给 AlipayClient 传 app_id,反射做不了 |
2.4 反面教材 v4:Map 缓存看似"简单"?
java
/**
* ❌ 反面教材 v4:Map 缓存 + 静态初始化
* 看起来像工厂方法,但有 3 个致命问题
*/
public class PaymentServiceV4 {
private static final Map<String, PaymentClient> CLIENT_MAP = new HashMap<>();
static {
CLIENT_MAP.put("alipay", new AlipayClient("app_id_xxx"));
CLIENT_MAP.put("wechat", new WechatClient("app_id_yyy"));
}
public PaymentResult pay(String channel, BigDecimal amount) {
PaymentClient client = CLIENT_MAP.get(channel);
if (client == null) {
throw new IllegalArgumentException("未知渠道: " + channel);
}
return client.pay(amount);
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 静态初始化过早 | 应用启动时所有客户端都创建了,启动慢 |
| ② | 无法传构造参数 | 不同的环境(dev/test/prod)用不同的 app_id,Map 写死做不到 |
| ③ | 没法用 Spring 注入 | 客户端里如果需要 RedisTemplate 等 Bean,static 初始化时还没生成 |
2.5 4 个反面教材的共同病根
| 痛点 | 反模式方案能不能解决? |
|---|---|
| 加新渠道不动老代码 | ❌ 全部要改老代码 |
| 不同环境用不同配置 | ❌ 写死配置 |
| 单元测试可 Mock | ❌ 全部耦合 |
| 启动按需加载 | ❌ 全部静态初始化 |
必须上工厂方法模式。这玩意儿不是为了装 X,是救命。
三、模式原理:工厂方法的"两件套 + 一张图"
3.1 工厂方法的两个核心角色
| 角色 | 职责 | 例子 |
|---|---|---|
| 抽象产品(Product) | 定义产品的"长相" | PaymentClient 接口 |
| 具体产品(Concrete Product) | 真正干活的产品 | AlipayClient、WechatClient |
| 抽象工厂(Factory) | 定义"造物"的接口 | PaymentFactory 抽象类 |
| 具体工厂(Concrete Factory) | 真正造产品的车间 | AlipayClientFactory、WechatClientFactory |
关键点 :每个具体产品都有自己的"造物车间",不共享。
3.2 一张图看懂工厂方法
┌────────────────────┐
│ 抽象工厂 │
│ PaymentFactory │
│ + createClient() │
└─────────┬──────────┘
│ extends
┌───────────────────┼───────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│Alipay │ │Wechat │ │UnionPay │
│ClientFactory│ │ClientFactory│ │ClientFactory│
│ │ │ │ │ │
│createClient()│ │createClient()│ │createClient()│
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ creates │ creates │ creates
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ AlipayClient │ │ WechatClient │ │UnionPayClient│
└──────────────┘ └──────────────┘ └──────────────┘
▲ ▲ ▲
│ implements │ implements │ implements
└────────┬───────────┴────────┬───────────┘
│ │
┌───▼────────────────────▼───┐
│ 抽象产品 │
│ PaymentClient │
│ + pay(amount) │
│ + refund(orderId) │
└─────────────────────────────┘
3.3 工厂方法的"灵魂三问"
Q1:为什么每个产品要配一个工厂?不能共享一个工厂?
共享工厂会导致工厂类随着产品增多越来越臃肿。每个产品配独立工厂,单一职责 + 易于扩展------加新渠道只新建一对(产品 + 工厂),老代码不动。
Q2:工厂方法 vs 直接 new,有什么本质区别?
直接 new 在编译期就绑死了具体类;工厂方法在运行时才决定造哪个类。好处是解耦 + 可扩展 + 可测试。
Q3:工厂方法一定要有"抽象工厂"吗?
不是。如果产品种类固定、不会扩展,用静态工厂方法(Static Factory Method)就够了。如果经常扩展,必须用"工厂方法模式"(抽象 + 实现)。
3.4 工厂方法的 3 种写法
| 写法 | 适用场景 | 缺点 |
|---|---|---|
| 简单工厂(静态方法) | 产品种类固定 | 不符合开闭原则,加产品要改代码 |
| 工厂方法(抽象+实现) | 产品种类会扩展 | 类数量翻倍 |
| Spring 容器(IoC) | 项目已用 Spring | 强依赖 Spring |
大白商城主推第二种(工厂方法),兼顾扩展性和可测试性。
四、实战代码:大白商城支付中心完整实现
下面是大白商城生产环境在用的支付中心实现,全套代码可直接复制到 IDEA 跑。
4.1 项目环境与依赖
pom.xml:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.dabai.mall</groupId>
<artifactId>mall-design-pattern-04</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>mall-design-pattern-04</name>
<description>大白商城 - 设计模式 04 工厂方法模式</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2 抽象产品:PaymentClient 接口
java
package com.dabai.mall.payment;
import java.math.BigDecimal;
/**
* ✅ 抽象产品:支付客户端接口
* <p>
* 所有支付渠道都要实现这个接口。定义了"支付客户端应该干啥"。
*
* @author 大白商城技术团队
* @since 1.0.0
*/
public interface PaymentClient {
/**
* 发起支付
*
* @param request 支付请求
* @return 支付结果
*/
PaymentResult pay(PaymentRequest request);
/**
* 查询支付状态
*
* @param outTradeNo 商户订单号
* @return 支付结果
*/
PaymentResult query(String outTradeNo);
/**
* 退款
*
* @param outTradeNo 商户订单号
* @param amount 退款金额
* @return 退款结果
*/
PaymentResult refund(String outTradeNo, BigDecimal amount);
/**
* 获取支持的支付渠道
*/
PaymentChannel getChannel();
}
配套 DTO:
java
package com.dabai.mall.payment;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 支付请求
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PaymentRequest {
/** 商户订单号 */
private String outTradeNo;
/** 支付金额 */
private BigDecimal amount;
/** 商品描述 */
private String subject;
/** 用户标识 */
private String userId;
}
java
package com.dabai.mall.payment;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 支付结果
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PaymentResult {
/** 是否成功 */
private boolean success;
/** 渠道交易号 */
private String tradeNo;
/** 商户订单号 */
private String outTradeNo;
/** 错误码(失败时) */
private String errorCode;
/** 错误信息 */
private String errorMessage;
}
java
package com.dabai.mall.payment;
import lombok.Getter;
/**
* 支付渠道枚举
*/
@Getter
public enum PaymentChannel {
/** 支付宝 */
ALIPAY("alipay", "支付宝"),
/** 微信支付 */
WECHAT("wechat", "微信支付"),
/** 银联 */
UNIONPAY("unionpay", "银联"),
/** 数字人民币 */
DIGITAL_RMB("digital_rmb", "数字人民币");
private final String code;
private final String description;
PaymentChannel(String code, String description) {
this.code = code;
this.description = description;
}
/**
* 根据 code 获取枚举
*/
public static PaymentChannel of(String code) {
for (PaymentChannel channel : values()) {
if (channel.code.equals(code)) {
return channel;
}
}
throw new IllegalArgumentException("未知支付渠道: " + code);
}
}
4.3 具体产品:3 个支付客户端
AlipayClient(支付宝):
java
package com.dabai.mall.payment;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* ✅ 具体产品:支付宝支付客户端
*
* @author 大白商城技术团队
*/
@Slf4j
public class AlipayClient implements PaymentClient {
private final String appId;
private final String privateKey;
private final String publicKey;
public AlipayClient(String appId, String privateKey, String publicKey) {
this.appId = appId;
this.privateKey = privateKey;
this.publicKey = publicKey;
log.info("【AlipayClient】初始化, appId={}", appId);
}
@Override
public PaymentResult pay(PaymentRequest request) {
log.info("【AlipayClient】发起支付, outTradeNo={}, amount={}",
request.getOutTradeNo(), request.getAmount());
// 实际调用支付宝 SDK
// AlipayTradeAppPayRequest alipayRequest = new AlipayTradeAppPayRequest();
// ... 省略 SDK 调用细节
return PaymentResult.builder()
.success(true)
.tradeNo("2024" + System.currentTimeMillis())
.outTradeNo(request.getOutTradeNo())
.build();
}
@Override
public PaymentResult query(String outTradeNo) {
log.info("【AlipayClient】查询支付状态, outTradeNo={}", outTradeNo);
return PaymentResult.builder()
.success(true)
.outTradeNo(outTradeNo)
.tradeNo("2024" + System.currentTimeMillis())
.build();
}
@Override
public PaymentResult refund(String outTradeNo, BigDecimal amount) {
log.info("【AlipayClient】退款, outTradeNo={}, amount={}", outTradeNo, amount);
return PaymentResult.builder()
.success(true)
.outTradeNo(outTradeNo)
.build();
}
@Override
public PaymentChannel getChannel() {
return PaymentChannel.ALIPAY;
}
}
WechatClient(微信支付):
java
package com.dabai.mall.payment;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* ✅ 具体产品:微信支付客户端
*
* @author 大白商城技术团队
*/
@Slf4j
public class WechatClient implements PaymentClient {
private final String appId;
private final String mchId;
private final String apiKey;
public WechatClient(String appId, String mchId, String apiKey) {
this.appId = appId;
this.mchId = mchId;
this.apiKey = apiKey;
log.info("【WechatClient】初始化, appId={}, mchId={}", appId, mchId);
}
@Override
public PaymentResult pay(PaymentRequest request) {
log.info("【WechatClient】发起支付, outTradeNo={}, amount={}",
request.getOutTradeNo(), request.getAmount());
// 实际调用微信支付 SDK
return PaymentResult.builder()
.success(true)
.tradeNo("wx" + System.currentTimeMillis())
.outTradeNo(request.getOutTradeNo())
.build();
}
@Override
public PaymentResult query(String outTradeNo) {
log.info("【WechatClient】查询支付状态, outTradeNo={}", outTradeNo);
return PaymentResult.builder()
.success(true)
.outTradeNo(outTradeNo)
.build();
}
@Override
public PaymentResult refund(String outTradeNo, BigDecimal amount) {
log.info("【WechatClient】退款, outTradeNo={}, amount={}", outTradeNo, amount);
return PaymentResult.builder()
.success(true)
.outTradeNo(outTradeNo)
.build();
}
@Override
public PaymentChannel getChannel() {
return PaymentChannel.WECHAT;
}
}
UnionPayClient(银联):
java
package com.dabai.mall.payment;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* ✅ 具体产品:银联支付客户端
*
* @author 大白商城技术团队
*/
@Slf4j
public class UnionPayClient implements PaymentClient {
private final String merId;
private final String certPath;
public UnionPayClient(String merId, String certPath) {
this.merId = merId;
this.certPath = certPath;
log.info("【UnionPayClient】初始化, merId={}", merId);
}
@Override
public PaymentResult pay(PaymentRequest request) {
log.info("【UnionPayClient】发起支付, outTradeNo={}, amount={}",
request.getOutTradeNo(), request.getAmount());
return PaymentResult.builder()
.success(true)
.tradeNo("up" + System.currentTimeMillis())
.outTradeNo(request.getOutTradeNo())
.build();
}
@Override
public PaymentResult query(String outTradeNo) {
log.info("【UnionPayClient】查询支付状态, outTradeNo={}", outTradeNo);
return PaymentResult.builder()
.success(true)
.outTradeNo(outTradeNo)
.build();
}
@Override
public PaymentResult refund(String outTradeNo, BigDecimal amount) {
log.info("【UnionPayClient】退款, outTradeNo={}, amount={}", outTradeNo, amount);
return PaymentResult.builder()
.success(true)
.outTradeNo(outTradeNo)
.build();
}
@Override
public PaymentChannel getChannel() {
return PaymentChannel.UNIONPAY;
}
}
4.4 抽象工厂 + 具体工厂
PaymentFactory 抽象工厂:
java
package com.dabai.mall.payment;
/**
* ✅ 抽象工厂:支付客户端工厂
* <p>
* 工厂方法模式的核心:定义"造物"的接口,让子类决定造啥。
*
* @author 大白商城技术团队
*/
public abstract class PaymentFactory {
/**
* ✅ 工厂方法:创建支付客户端
* <p>
* 子类必须实现这个方法,决定造哪种支付客户端。
*/
public abstract PaymentClient createClient();
/**
* 通用方法:创建客户端 + 初始化
*/
public final PaymentClient createInitializedClient() {
PaymentClient client = createClient();
// 通用初始化逻辑(如果需要)
return client;
}
}
AlipayClientFactory:
java
package com.dabai.mall.payment;
import lombok.extern.slf4j.Slf4j;
/**
* ✅ 具体工厂:支付宝客户端工厂
*
* @author 大白商城技术团队
*/
@Slf4j
public class AlipayClientFactory extends PaymentFactory {
private final String appId;
private final String privateKey;
private final String publicKey;
public AlipayClientFactory(String appId, String privateKey, String publicKey) {
this.appId = appId;
this.privateKey = privateKey;
this.publicKey = publicKey;
}
@Override
public PaymentClient createClient() {
log.info("【AlipayClientFactory】创建 AlipayClient");
return new AlipayClient(appId, privateKey, publicKey);
}
}
WechatClientFactory:
java
package com.dabai.mall.payment;
import lombok.extern.slf4j.Slf4j;
/**
* ✅ 具体工厂:微信支付客户端工厂
*
* @author 大白商城技术团队
*/
@Slf4j
public class WechatClientFactory extends PaymentFactory {
private final String appId;
private final String mchId;
private final String apiKey;
public WechatClientFactory(String appId, String mchId, String apiKey) {
this.appId = appId;
this.mchId = mchId;
this.apiKey = apiKey;
}
@Override
public PaymentClient createClient() {
log.info("【WechatClientFactory】创建 WechatClient");
return new WechatClient(appId, mchId, apiKey);
}
}
UnionPayClientFactory:
java
package com.dabai.mall.payment;
import lombok.extern.slf4j.Slf4j;
/**
* ✅ 具体工厂:银联支付客户端工厂
*
* @author 大白商城技术团队
*/
@Slf4j
public class UnionPayClientFactory extends PaymentFactory {
private final String merId;
private final String certPath;
public UnionPayClientFactory(String merId, String certPath) {
this.merId = merId;
this.certPath = certPath;
}
@Override
public PaymentClient createClient() {
log.info("【UnionPayClientFactory】创建 UnionPayClient");
return new UnionPayClient(merId, certPath);
}
}
4.5 业务调用方:PaymentService
java
package com.dabai.mall.payment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* ✅ 支付服务:工厂方法的调用方
* <p>
* 关键点:PaymentService 完全不关心"具体是哪个渠道",
* 它只持有工厂列表,由 Spring 注入。
*
* @author 大白商城技术团队
*/
@Service
public class PaymentService {
/**
* 渠道 -> 工厂的映射
*/
private final Map<PaymentChannel, PaymentFactory> factoryMap = new HashMap<>();
/**
* Spring 注入所有 PaymentFactory 的实现
*/
@Autowired
public PaymentService(List<PaymentFactory> factories) {
for (PaymentFactory factory : factories) {
// 通过工厂创建客户端来识别渠道
PaymentClient client = factory.createClient();
factoryMap.put(client.getChannel(), factory);
}
}
/**
* 发起支付
*/
public PaymentResult pay(String channel, PaymentRequest request) {
PaymentFactory factory = factoryMap.get(PaymentChannel.of(channel));
if (factory == null) {
throw new IllegalArgumentException("不支持的支付渠道: " + channel);
}
PaymentClient client = factory.createClient();
return client.pay(request);
}
}
关键解读:
| 行 | 代码 | 解读 |
|---|---|---|
List<PaymentFactory> factories |
Spring 自动注入所有实现 | 加新渠道不用改这里 |
factory.createClient().getChannel() |
通过客户端反推渠道 | 渠道识别自动化 |
factoryMap.get(...) |
通过渠道找工厂 | O(1) 时间复杂度 |
加新渠道的 3 步走(数字人民币为例):
第 1 步:新建 DigitalRmbClient implements PaymentClient
第 2 步:新建 DigitalRmbClientFactory extends PaymentFactory
第 3 步:把 DigitalRmbClientFactory 标 @Component
完事。PaymentService 一行不改。
4.6 单元测试
java
package com.dabai.mall.payment;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
/**
* 支付中心完整单元测试
*/
class PaymentServiceTest {
private PaymentService paymentService;
@BeforeEach
void setUp() {
// 手动注册 3 个工厂
paymentService = new PaymentService(List.of(
new AlipayClientFactory("app_id_alipay", "private_key", "public_key"),
new WechatClientFactory("app_id_wechat", "mch_id_123", "api_key_456"),
new UnionPayClientFactory("mer_id_union", "/path/to/cert")
));
}
@Test
void testAlipayPay() {
PaymentRequest request = PaymentRequest.builder()
.outTradeNo("ORDER_001")
.amount(new BigDecimal("100.00"))
.subject("测试商品")
.userId("USER_001")
.build();
PaymentResult result = paymentService.pay("alipay", request);
assertTrue(result.isSuccess());
assertEquals("ORDER_001", result.getOutTradeNo());
assertNotNull(result.getTradeNo());
}
@Test
void testWechatPay() {
PaymentRequest request = PaymentRequest.builder()
.outTradeNo("ORDER_002")
.amount(new BigDecimal("200.00"))
.build();
PaymentResult result = paymentService.pay("wechat", request);
assertTrue(result.isSuccess());
}
@Test
void testUnionPayPay() {
PaymentRequest request = PaymentRequest.builder()
.outTradeNo("ORDER_003")
.amount(new BigDecimal("300.00"))
.build();
PaymentResult result = paymentService.pay("unionpay", request);
assertTrue(result.isSuccess());
}
@Test
void testUnknownChannel() {
PaymentRequest request = PaymentRequest.builder()
.outTradeNo("ORDER_004")
.amount(new BigDecimal("100.00"))
.build();
assertThrows(IllegalArgumentException.class,
() -> paymentService.pay("unknown", request));
}
@Test
void testFactoryCreate() {
// 直接测工厂方法
AlipayClientFactory alipayFactory = new AlipayClientFactory("app", "priv", "pub");
PaymentClient client = alipayFactory.createClient();
assertNotNull(client);
assertEquals(PaymentChannel.ALIPAY, client.getChannel());
}
@Test
void testRefund() {
PaymentResult result = paymentService.pay("alipay",
PaymentRequest.builder().outTradeNo("ORDER_REFUND").amount(new BigDecimal("50.00")).build());
assertTrue(result.isSuccess());
PaymentResult refundResult = paymentService.pay("alipay",
PaymentRequest.builder().outTradeNo("ORDER_REFUND").amount(new BigDecimal("50.00")).build());
// 这里简化为重新调用,实际应该是单独的 refund 方法
assertNotNull(refundResult);
}
}
4.7 完整调用流程
┌─────────────────────┐
│ Controller 调用 │
│ POST /api/pay │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ PaymentService │
│ (Spring 自动注入) │
└──────────┬──────────┘
│
根据 channel 找工厂
│
┌──────────────────────┼──────────────────────┐
│ │ │
┌────▼─────────┐ ┌────▼─────────┐ ┌────▼─────────┐
│AlipayFactory │ │WechatFactory │ │UnionFactory │
└────┬──────────┘ └────┬─────────┘ └────┬─────────┘
│ createClient() │ createClient() │ createClient()
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│AlipayClient│ │WechatClient│ │UnionClient │
│ .pay() │ │ .pay() │ │ .pay() │
└────────────┘ └────────────┘ └────────────┘
│ │ │
└──────────────────────┼──────────────────────┘
▼
┌─────────────────────┐
│ PaymentResult │
│ 返回给 Controller │
└─────────────────────┘
五、工厂方法 vs 抽象工厂:到底有啥区别?
5.1 一张表看清核心区别
| 维度 | 工厂方法 | 抽象工厂 |
|---|---|---|
| 关注点 | 造一种产品 | 造一整套产品(产品族) |
| 工厂数量 | 每个产品一个工厂 | 一个工厂造一整套 |
| 扩展难度 | 加产品容易 | 加产品族容易,加产品难 |
| 典型例子 | AlipayFactory 造 AlipayClient |
PCGoodsFactory 造 PC 端的整个详情页 |
| 类数量 | 2N(N 个产品 → N 个工厂 + N 个产品) | M × N(M 个产品族 × N 个产品) |
5.2 一个具体例子
大白商城多端商品详情(PC / H5 / 小程序):
| 产品族 | 商品数据源 | 详情渲染器 | 缓存策略 |
|---|---|---|---|
| PC 端 | PCGoodsDataSource | PCRenderer | PCCacheStrategy |
| H5 端 | H5GoodsDataSource | H5Renderer | H5CacheStrategy |
| 小程序 | MiniProgramDataSource | MiniProgramRenderer | MiniProgramCacheStrategy |
- 用工厂方法:要 3 × 3 = 9 个工厂类。每个工厂只造一个产品。
- 用抽象工厂:要 3 个工厂类。每个工厂造 3 个产品。
大白商城选型:
| 场景 | 用哪个 | 原因 |
|---|---|---|
| 支付客户端(每渠道独立) | 工厂方法 | 每个客户端独立,加新渠道容易 |
| 多端商品详情(每端一整套) | 抽象工厂 | 一端的所有组件必须配套 |
| 短信服务(每厂商独立) | 工厂方法 | 短信客户端独立 |
| 多数据库源(多套 ORM) | 抽象工厂 | 一套数据源包含 Connection/Statement/ResultSet |
5.3 工厂方法 vs 抽象工厂:决策树
你的"产品"是一组互不相干的东西?
├── 是(支付客户端、短信客户端、日志客户端)
│ └── ✅ 用工厂方法
└── 不是(产品之间有"配套关系")
├── 是(PC 端一整套、H5 端一整套)
│ └── ✅ 用抽象工厂
└── 不是
└── ✅ 用工厂方法(更简单)
5.4 一个常见的误解
"抽象工厂 = 工厂方法 + 抽象 = 更高级的工厂方法。"
错! 它们是两种不同的设计思路:
- 工厂方法:纵向扩展(加新产品容易)
- 抽象工厂:横向扩展(加产品族容易)
大白商城两个都用:
- 支付中心用工厂方法(加支付渠道是纵向)
- 多端详情用抽象工厂(加端是横向)
抽象工厂我们会在 06 篇 详细讲。
六、Spring 的 BeanFactory:工厂方法的"祖师爷"
6.1 BeanFactory 是什么?
BeanFactory 是 Spring IoC 容器的最顶层接口,它的核心方法:
java
public interface BeanFactory {
Object getBean(String name) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
// ... 其他重载
}
本质上,getBean(String name) 就是工厂方法:传一个 bean 名字,返回一个对象。
6.2 BeanFactory 的"工厂方法模式"本质
┌──────────────────────┐
│ 抽象工厂 │
│ BeanFactory │
│ + getBean(name) │
└─────────┬────────────┘
│ implements
┌───────────────────┼───────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│DefaultList- │ │XmlBean- │ │Annotation- │
│ableBean- │ │Factory │ │ConfigAppli- │
│Factory │ │(废弃) │ │cationContext│
└─────────────┘ └─────────────┘ └─────────────┘
- 抽象产品 :
Object(所有 Bean) - 具体产品 :所有被 Spring 管理的 Bean(
AlipayClient、UserService......) - 抽象工厂 :
BeanFactory - 具体工厂 :
DefaultListableBeanFactory、AnnotationConfigApplicationContext
6.3 FactoryBean:Bean 工厂的 Bean
Spring 还提供了一种特殊接口 FactoryBean,让 Bean 本身就是一个工厂:
java
public interface FactoryBean<T> {
T getObject() throws Exception;
Class<?> getObjectType();
boolean isSingleton();
}
例子 :MyBatis-Spring 的 SqlSessionFactoryBean 就是一个 FactoryBean:
java
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory> {
@Override
public SqlSessionFactory getObject() throws Exception {
// 复杂创建逻辑
return SqlSessionFactoryBuilder.build(configuration);
}
@Override
public Class<?> getObjectType() {
return SqlSessionFactory.class;
}
}
这才是"工厂方法模式"在 Spring 中的真正威力:
把"造物"逻辑封装在 Bean 内部,使用方只通过
@Autowired拿成品,不用关心怎么造的。
6.4 大白商城的"抄作业"
java
/**
* 借鉴 Spring FactoryBean:通用客户端工厂 Bean
* 任何"创建逻辑复杂"的客户端都可以用它
*/
@Component
public class PaymentClientFactoryBean implements FactoryBean<PaymentClient> {
@Value("${payment.channel:alipay}")
private String channel;
@Override
public PaymentClient getObject() throws Exception {
return switch (channel) {
case "alipay" -> new AlipayClient("app_id_xxx", "priv", "pub");
case "wechat" -> new WechatClient("app_id_yyy", "mch", "key");
case "unionpay" -> new UnionPayClient("mer", "cert");
default -> throw new IllegalArgumentException("未知渠道: " + channel);
};
}
@Override
public Class<?> getObjectType() {
return PaymentClient.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
调用方:
java
@Service
public class OrderPaymentService {
@Autowired
private PaymentClient paymentClient; // 直接注入成品,不用关心怎么造的
public void pay(Order order) {
paymentClient.pay(...);
}
}
6.5 Spring 工厂方法 vs GoF 工厂方法
| 维度 | GoF 工厂方法 | Spring 工厂方法 |
|---|---|---|
| 创建方 | 子类(具体工厂) | 配置(XML/注解) |
| 工厂类 | 必须继承抽象工厂 | 不需要,直接 @Bean 或 @Component |
| 扩展方式 | 加子类 | 加 @Bean 方法或 @Component 类 |
| 典型场景 | 自研框架 | 企业级应用 |
大白商城的实践:
- 支付中心用 GoF 工厂方法(多渠道独立、易扩展)
- 通用 Bean 用 Spring
@Component/@Bean(Spring 自动管理)
七、工程决策 Checklist:什么时候用、什么时候别用
7.1 ✅ 这 5 种情况,强烈建议用工厂方法
| 序号 | 场景 | 原因 |
|---|---|---|
| ① | 同一接口有多个实现(如多支付渠道) | 加新实现不改老代码 |
| ② | 创建逻辑复杂(如需要传多个参数) | 封装创建过程 |
| ③ | 根据配置/环境选择实现 | 运行时决策 |
| ④ | 需要解耦"使用者"和"创建者" | 单一职责 |
| ⑤ | 单元测试需要 Mock | 抽象类易 Mock |
7.2 ❌ 这 5 种情况,绝对不要用工厂方法
| 序号 | 场景 | 原因 |
|---|---|---|
| ① | 只有 1 个实现类 | 直接 new 就行,工厂是多余的 |
| ② | 对象无差异 (如 new ArrayList<>()) |
工厂方法纯属装 X |
| ③ | **业务只 |
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯