大白话说Java设计模式-04-工厂方法模式(业务实战篇)

大白话说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) 真正干活的产品 AlipayClientWechatClient
抽象工厂(Factory) 定义"造物"的接口 PaymentFactory 抽象类
具体工厂(Concrete Factory) 真正造产品的车间 AlipayClientFactoryWechatClientFactory

关键点每个具体产品都有自己的"造物车间",不共享。

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 一张表看清核心区别

维度 工厂方法 抽象工厂
关注点 一种产品 一整套产品(产品族)
工厂数量 每个产品一个工厂 一个工厂造一整套
扩展难度 加产品容易 加产品族容易,加产品难
典型例子 AlipayFactoryAlipayClient 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(AlipayClientUserService......)
  • 抽象工厂BeanFactory
  • 具体工厂DefaultListableBeanFactoryAnnotationConfigApplicationContext

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
**业务只

觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
BUG指挥官1 小时前
Sa-Token和Spring Security对比
java·后端·spring
小当家.1052 小时前
深入理解 ReAct Agent:从原理到 Java 实战
java·react.js·agent·react·架构设计·agent设计
geminigoth2 小时前
Spring AI Alibaba 入门开发一(备份)
java·人工智能·spring
Hammer_Hans2 小时前
DFT笔记98
java·开发语言·数据库
古法安卓2 小时前
Android-DeviceStorageMonitorService 流程分析
java·面试·android studio
Java内核笔记2 小时前
Spring Boot 4 拥抱 Jackson 3:包名迁移、配置改名与自动配置源码剖析
java·后端
程序员黑豆3 小时前
什么是JDK以及JDK都由哪些部分组成呢
java·前端·ai编程
估值探索者3 小时前
【Python实时盯盘与预警 #08】成交额突然放大2倍?Python窗口比较抓异动
java·开发语言·python
凤山老林3 小时前
Spring Boot 定时任务进阶:动态 Cron 与集群防重实战
java·spring boot·后端·定时任务·集群定时任务