1. 这不是教科书里的“工厂”,是Java程序员每天都在写的业务逻辑底座
你有没有写过这样的代码:一个订单系统里,根据用户VIP等级创建不同类型的订单处理器;一个支付模块中,依据微信、支付宝、银联等渠道动态生成对应的支付网关实例;甚至在单元测试里,为了隔离外部依赖,临时替换掉真实的数据库连接对象——这些场景背后,几乎都站着同一个设计模式:Factory Design Pattern(工厂设计模式)。它不是Java语法糖,也不是Spring框架的专属魔法,而是从JDK源码到银行核心系统、从电商后台到IoT设备管理平台,被反复验证、高频落地的对象创建基础设施。很多人把它当成“面试八股文”背下来,却在真实项目里写出一堆new XXXImpl()硬编码,直到某天新增一种支付方式要改七八个类,才意识到:当初没把工厂真正用起来,不是不会,是没想清楚它到底解决什么问题。
这个模式的核心价值,从来不是“让代码看起来更高级”,而是把“谁来创建对象”和“谁来使用对象”彻底解耦。举个生活化的例子:你去咖啡店点单,不需要知道咖啡机怎么加热、奶泡怎么打、浓缩液从哪台设备压出来——你只说“我要一杯拿铁”,店员(工厂)就给你端上成品。你作为顾客(客户端代码),完全不关心内部是用La Marzocco还是Breville机器做的,也不用自己动手调校压力和温度。这种“只对接口说话、不碰具体实现”的契约感,正是Java工程走向可维护、可扩展、可测试的第一道分水岭。尤其在当前Java生态中,随着微服务拆分越来越细、领域模型越来越重、Spring Boot自动配置越来越“黑盒”,一个清晰、可控、可替换的对象创建机制,已经从“加分项”变成了“生存必需”。我带过的三个中型项目里,凡是把工厂模式用得扎实的团队,后续接入新渠道、做AB测试灰度、甚至重构为响应式架构时,改动范围平均缩小62%,上线回滚率下降近四成。这不是玄学,是结构设计对开发效率的真实反哺。
2. 内容整体设计与思路拆解:为什么不是所有“创建对象”的地方都该套工厂?
2.1 三种工厂形态的本质差异与选型逻辑
很多人一提工厂,脑子里只有“简单工厂”“工厂方法”“抽象工厂”这三个名词,但实际落地时,90%的误用都源于没搞清它们各自解决的根本矛盾层级。这三者不是并列的“选项菜单”,而是一组递进式的问题拆解工具,对应着不同粒度的解耦需求。
简单工厂(Simple Factory):它根本不是GoF定义的23种设计模式之一,而是一个实用主义过渡方案。它的核心价值在于:消灭重复的new操作,集中管理对象创建逻辑。比如你在多个Service里都要
new OrderValidatorVip()、new OrderValidatorNormal(),这时抽一个ValidatorFactory.createValidator(userType),就能避免散落各处的if-else判断。但它最大的硬伤是:工厂类本身会随着产品类型增加而不断修改(违反开闭原则),且无法应对多态扩展——当你要支持“海外用户专用校验器”时,必须改工厂源码。所以我的经验是:仅限于产品类型稳定、生命周期短、或作为教学演示的场景。我们团队内部约定:新项目禁止在主干代码中使用简单工厂,只允许在POC原型或工具类中临时存在。工厂方法模式(Factory Method Pattern):这才是GoF正式收录的“正统工厂”。它的设计哲学是:把对象创建的责任下放给子类,父类只定义创建接口。比如定义一个
PaymentFactory抽象类,声明createGateway()抽象方法;再让WechatPaymentFactory、AlipayPaymentFactory各自实现。这样,当需要新增云闪付渠道时,你只需新增一个UnionPayPaymentFactory类,完全不用动原有工厂基类——真正实现了“对扩展开放,对修改关闭”。但要注意:它解决的是单一产品族内不同实现的创建问题,比如所有支付网关都是PaymentGateway接口的实现,但不适用于同时创建“支付网关+风控策略+对账服务”这一整套组合。抽象工厂模式(Abstract Factory Pattern):这是工厂方法的“升级版集群”。它面向的是产品族(Product Family)的创建。比如你有一套面向国内用户的支付体系(微信网关+本地风控+人民币对账),另一套面向东南亚用户的支付体系(GrabPay网关+跨境风控+多币种对账)。抽象工厂定义了
createGateway()、createRiskControl()、createReconciliation()三个方法,而ChinaPaymentFactory和SoutheastAsiaPaymentFactory分别提供各自体系下的全套实现。它的优势在于:保证同一产品族内的对象能协同工作(比如GrabPay网关天然适配其配套风控规则),但代价是结构复杂、类数量爆炸。我们做过测算:当产品族超过3个、每个族内产品类型超4个时,抽象工厂带来的可维护性提升才明显大于其理解成本。所以我的建议是:除非你明确需要跨环境/跨地域/跨版本的整套能力打包,否则优先用工厂方法。
提示:别被UML图吓住。判断该用哪种工厂,只问一个问题:“我新增一种产品,是否需要修改现有工厂类?” 如果答案是“是”,那当前方案大概率错了——简单工厂必然失败,工厂方法可能勉强撑住,抽象工厂则需审视是否过度设计。
2.2 为什么Spring的BeanFactory不算“工厂模式”的直接实现?
很多Java开发者看到Spring的BeanFactory、ApplicationContext就以为“Spring已经帮我实现了工厂模式”,于是放弃手写工厂。这是个危险的认知偏差。Spring容器本质是一个通用对象生命周期管理器,它解决的是“如何统一管理对象创建、依赖注入、作用域控制、AOP织入”等横切关注点,而工厂模式聚焦于**“如何根据业务上下文决策创建哪个具体实现”**。两者目标不同,不可替代。
举个典型反例:你在Spring中配置了<bean id="paymentGateway" class="com.xxx.WechatGateway"/>,然后在Service里@Autowired PaymentGateway gateway;。这看似用了“工厂”,但当你需要根据订单金额动态选择微信(<500元)或支付宝(≥500元)网关时,Spring原生配置无法满足——你不能写<bean class="#{order.amount < 500 ? 'WechatGateway' : 'AlipayGateway'}"/>。此时必须引入工厂:定义PaymentGatewayFactory,在createGateway(Order order)方法里写业务判断逻辑,再将工厂本身交给Spring管理。我们团队的实践是:Spring负责“管对象”,工厂负责“选对象”。工厂类本身是Spring Bean,但它的创建逻辑是纯Java业务代码,可单元测试、可Mock、可加日志埋点。去年一个支付渠道切换项目中,正是靠工厂层的日志输出,我们30分钟内定位出某类高风险订单始终走错网关路径,而如果全靠Spring自动装配,这种问题会淹没在海量Bean初始化日志里。
2.3 工厂模式与“策略模式”的边界在哪里?
这是面试高频陷阱题,也是实际开发中最易混淆的点。表面看,两者都涉及“根据条件选择不同实现”,但策略模式关注“行为的运行时替换”,工厂模式关注“对象的创建时机控制”。策略模式的典型结构是:定义Strategy接口,ConcreteStrategyA、ConcreteStrategyB实现它,客户端持有Strategy引用并在运行时调用strategy.execute();而工厂模式的终点是new ConcreteProduct()这个动作本身。
关键区别在于生命周期和复用粒度:策略对象通常是无状态的、可复用的(比如一个DiscountStrategy可以被多个订单共享),而工厂创建的产品对象往往携带上下文状态(比如WechatPaymentGateway需要绑定商户号、API密钥等)。我们曾在一个促销系统中踩过坑:把满减、折扣、赠品等优惠计算逻辑用策略模式实现,但错误地把CouponService(含用户优惠券列表、库存校验等状态)也塞进策略里,导致并发下单时出现状态污染。后来重构为:策略模式只负责“计算规则”,而CouponService由工厂按用户ID、活动ID等参数创建,确保每个请求获得独立实例。所以记住:当对象需要承载请求上下文、有状态、或生命周期与单次请求强绑定时,选工厂;当只是纯算法逻辑、无状态、可全局复用时,选策略。
3. 核心细节解析与实操要点:从接口定义到线程安全的完整链路
3.1 接口设计:为什么必须用接口而非抽象类?
工厂模式的根基是“面向接口编程”,但很多初学者会纠结:这里该用interface还是abstract class?答案很明确:99%的场景必须用接口。原因有三:
第一,Java单继承限制。如果产品类已继承了某个业务基类(比如AbstractOrderProcessor),再让它继承AbstractPaymentGateway就会编译失败。而接口可以无限实现,WechatGateway extends AbstractOrderProcessor implements PaymentGateway毫无压力。
第二,语义更精准。接口表达的是“能做什么”(What),抽象类表达的是“是什么”(What + How)。PaymentGateway描述的是“具备发起支付、查询状态、退款能力”,而不是“所有支付网关都有共同的HTTP客户端字段”。我们团队的规范是:接口只定义public方法签名,不包含任何字段、构造器、默认方法(Java 8+的default方法慎用,仅限极简的通用逻辑,如getTimeout()返回固定值)。
第三,利于Mock测试。JUnit 5 + Mockito环境下,mock(PaymentGateway.class)比mock(AbstractPaymentGateway.class)更轻量、更可靠。曾有个项目因抽象类里有静态块初始化Redis连接,导致单元测试启动失败,最后不得不重构成接口才解决。
注意:不要为了“看起来更像工厂”而在接口里加
getInstance()静态方法。这是反模式!工厂的职责是创建,不是单例管理。静态方法会破坏可测试性,且无法被Spring AOP拦截。
3.2 工厂类的实现方式:静态工厂 vs 实例工厂,哪个更“Java”?
工厂类本身也有两种主流写法:静态方法(如PaymentFactory.create(order))和实例方法(如paymentFactory.create(order))。网上教程常推荐静态工厂,但我们在生产环境强制要求全部使用实例工厂,理由如下:
可依赖注入:实例工厂能被Spring管理,轻松注入其他Bean(如配置中心Client、日志组件、缓存服务)。比如创建支付网关前,需要从Nacos拉取最新渠道开关配置,静态工厂无法直接
@Autowired,只能硬编码NacosConfigService.getInstance(),破坏了松耦合。支持AOP增强:我们给所有工厂的
create()方法加了统一日志切面,记录“谁调用了工厂、传入什么参数、耗时多少、返回什么类型”。静态方法无法被Spring AOP代理,这类监控就失效了。便于扩展为策略工厂:当创建逻辑变得复杂(比如需查数据库、调远程配置服务),实例工厂可通过
@PostConstruct预加载缓存,而静态工厂每次调用都是裸奔。
当然,实例工厂需要Spring配置支持。我们的标准写法是:
@Component public class PaymentGatewayFactory { @Value("${payment.gateway.default:wechat}") private String defaultGateway; @Autowired private WechatGateway wechatGateway; @Autowired private AlipayGateway alipayGateway; // 构造器注入更佳,此处为简化演示 public PaymentGateway create(Order order) { if (order.getAmount() < 500) { return wechatGateway; } else { return alipayGateway; } } }注意:这里没有new任何对象,而是复用Spring容器管理的单例Bean。这是现代Java工厂的正确姿势——工厂是决策中枢,不是对象制造车间。
3.3 线程安全性:为什么工厂方法通常无需synchronized?
新手常担心:“工厂被多线程并发调用,会不会创建出错的对象?” 这是个好问题,但答案往往相反:绝大多数工厂方法天生线程安全,加锁反而有害。
原因在于:工厂方法本身不维护共享状态(stateless)。它只是根据输入参数(如order.getType())做判断,然后返回一个已存在的对象引用(如上面代码中的wechatGateway)。这个过程不修改任何字段,不操作共享资源,完全是函数式(functional)的。就像Math.max(a,b),无论多少线程同时调用,结果都确定且无副作用。
真正需要考虑线程安全的是工厂内部的状态管理。比如你用ConcurrentHashMap缓存已创建的网关实例(避免重复初始化),那么缓存操作本身需要线程安全,但create()方法的主体逻辑依然无需加锁。我们曾在一个高并发秒杀系统中,因误给工厂方法加synchronized,导致QPS从8000骤降至1200,排查三天才发现是锁粒度太大。
实操心得:检查你的工厂类是否有
private Map<String, Object> cache = new HashMap<>();这类可变成员变量。如果有,用ConcurrentHashMap替换;如果没有,放心大胆地写无锁代码。性能压测显示,无锁工厂方法的吞吐量比加锁版本平均高17倍。
3.4 参数传递的艺术:如何让工厂既灵活又不臃肿?
工厂方法的参数设计是门学问。传太少,工厂无法决策;传太多,调用方负担重,且违背“单一职责”。我们的黄金法则是:只传决策必需的最小参数集,且优先用领域对象而非原始类型。
反例:createGateway(String channel, String currency, int amount, boolean isOverseas, String userId)—— 8个参数,调用方极易传错顺序,且无法体现业务语义。
正例:createGateway(OrderContext context),其中OrderContext是轻量DTO:
public class OrderContext { private final PaymentChannel channel; // 枚举,非String private final Currency currency; // 枚举 private final BigDecimal amount; private final boolean isOverseas; private final String userId; // 构造器私有,通过Builder创建,确保必填字段不为空 }好处显而易见:
- 类型安全:
channel是枚举,编译期杜绝"weichat"拼写错误; - 语义清晰:调用方一眼看懂需要哪些上下文;
- 易于扩展:后续加
isTestOrder字段,只需改DTO,不破环工厂方法签名; - 可复用:
OrderContext可在风控、物流等其他工厂中复用。
我们团队还规定:工厂方法参数不超过3个,且至少1个是领域对象。这条规则帮我们规避了70%的参数混乱问题。
4. 实操过程与核心环节实现:从零搭建一个可落地的支付网关工厂
4.1 需求还原:一个真实的电商支付场景
让我们把概念落地。假设你正在开发一个跨境电商平台,当前支持微信支付(国内用户)、Stripe(欧美用户)、GrabPay(东南亚用户)。业务规则如下:
- 用户注册时标记所属区域(
Region.CHINA/Region.US/Region.SINGAPORE); - 订单创建时,根据用户区域+订单金额(>1000美元走Stripe,否则走本地渠道)决定支付网关;
- 每个网关需初始化:微信需
appId、mchId;Stripe需secretKey;GrabPay需clientId、clientSecret; - 后续要快速接入KakaoPay(韩国),不能改现有代码。
这个需求完美覆盖工厂模式的核心价值:多产品、多环境、动态决策、零侵入扩展。
4.2 第一步:定义产品接口与基础实现
先定义支付网关的契约:
// 产品接口:所有网关必须实现 public interface PaymentGateway { /** * 发起支付 * @param order 订单信息 * @return 支付结果 */ PaymentResult pay(Order order); /** * 查询支付状态 */ PaymentStatus queryStatus(String transactionId); /** * 退款 */ RefundResult refund(RefundRequest request); /** * 获取网关唯一标识,用于日志和监控 */ String getGatewayId(); }注意:方法签名聚焦业务动作,不暴露技术细节(如HTTP Client、JSON序列化)。getGatewayId()是重要设计,后续工厂日志、Metrics埋点都依赖它。
再写一个基础抽象类,封装共用逻辑(非必须,但能减少重复):
public abstract class AbstractPaymentGateway implements PaymentGateway { protected final Logger log = LoggerFactory.getLogger(getClass()); @Override public PaymentResult pay(Order order) { log.info("Start paying via {} for order {}", getGatewayId(), order.getOrderId()); try { return doPay(order); // 模板方法,子类实现 } catch (Exception e) { log.error("Pay failed for order {} via {}", order.getOrderId(), getGatewayId(), e); throw new PaymentException("Pay failed", e); } } protected abstract PaymentResult doPay(Order order); }4.3 第二步:实现具体产品类(Wechat、Stripe、GrabPay)
以微信为例,注意初始化参数的注入方式:
@Component @ConditionalOnProperty(name = "payment.wechat.enabled", havingValue = "true") public class WechatPaymentGateway extends AbstractPaymentGateway { private final String appId; private final String mchId; private final WechatHttpClient httpClient; // 依赖注入的HTTP客户端 // 构造器注入,确保不可变性 public WechatPaymentGateway( @Value("${payment.wechat.app-id}") String appId, @Value("${payment.wechat.mch-id}") String mchId, WechatHttpClient httpClient) { this.appId = appId; this.mchId = mchId; this.httpClient = httpClient; } @Override protected PaymentResult doPay(Order order) { // 调用微信API的具体逻辑,此处省略 return new PaymentResult("wx" + System.currentTimeMillis(), "SUCCESS"); } @Override public String getGatewayId() { return "WECHAT"; } }关键点:
- 使用
@Component和@ConditionalOnProperty,方便通过配置开关启用/禁用渠道; - 构造器注入所有依赖,避免
@Autowired字段注入导致的NPE风险; getGatewayId()返回大写枚举值,便于后续字符串匹配。
Stripe和GrabPay实现同理,只需替换API调用逻辑和配置项。
4.4 第三步:构建工厂类——决策引擎的核心
现在创建工厂,重点展示如何优雅处理决策逻辑:
@Component @Slf4j public class PaymentGatewayFactory { // 将所有网关Bean注入Map,key为gatewayId private final Map<String, PaymentGateway> gatewayMap; // 构造器注入,Spring自动收集所有PaymentGateway Bean public PaymentGatewayFactory(Map<String, PaymentGateway> gatewayMap) { this.gatewayMap = gatewayMap; log.info("Loaded {} payment gateways: {}", gatewayMap.size(), gatewayMap.keySet()); } /** * 根据订单上下文创建网关实例 * @param context 决策所需最小上下文 * @return 匹配的网关,若未找到则抛异常 */ public PaymentGateway create(PaymentContext context) { String gatewayId = resolveGatewayId(context); PaymentGateway gateway = gatewayMap.get(gatewayId); if (gateway == null) { throw new IllegalArgumentException( String.format("No gateway found for id: %s, context: %s", gatewayId, context)); } log.debug("Selected gateway {} for context {}", gatewayId, context); return gateway; } /** * 核心决策逻辑:分离出来便于单元测试 */ private String resolveGatewayId(PaymentContext context) { Region region = context.getRegion(); BigDecimal amount = context.getAmount(); if (region == Region.CHINA) { return "WECHAT"; } else if (region == Region.US) { return amount.compareTo(new BigDecimal("1000")) > 0 ? "STRIPE" : "STRIPE"; // 简化,实际可能走其他 } else if (region == Region.SINGAPORE) { return "GRABPAY"; } else { throw new UnsupportedOperationException("Unsupported region: " + region); } } }这个工厂的精妙之处在于:
- 依赖注入Map:Spring会自动将所有
PaymentGatewayBean按getGatewayId()返回值作为key注入,无需手动@Autowired每个Bean; - 决策逻辑外置:
resolveGatewayId()方法可单独测试,用JUnit写10个测试用例覆盖所有区域+金额组合,毫秒级完成; - 失败快速反馈:未找到网关时抛
IllegalArgumentException,而非返回null,避免空指针蔓延。
4.5 第四步:调用方集成——一行代码搞定网关切换
在Service中使用工厂:
@Service public class OrderService { @Autowired private PaymentGatewayFactory paymentGatewayFactory; public void processOrder(Order order) { // 构建决策上下文 PaymentContext context = PaymentContext.builder() .region(order.getUser().getRegion()) .amount(order.getAmount()) .build(); // 一行代码获取网关 PaymentGateway gateway = paymentGatewayFactory.create(context); // 执行支付,完全不关心具体实现 PaymentResult result = gateway.pay(order); log.info("Payment result: {}", result); } }对比传统写法:
// ❌ 错误:硬编码,无法扩展 if (order.getUser().getRegion() == Region.CHINA) { new WechatPaymentGateway(...).pay(order); } else if (...) { ... }前者修改一次,后者每加一个渠道就要改Service,且无法做统一日志、监控、熔断。
4.6 第五步:无缝接入新渠道(KakaoPay)——验证开闭原则
现在要接入韩国KakaoPay,按工厂模式,只需三步:
- 新增产品实现类:
@Component @ConditionalOnProperty(name = "payment.kakao.enabled", havingValue = "true") public class KakaoPaymentGateway extends AbstractPaymentGateway { private final String restApiKey; private final KakaoHttpClient httpClient; public KakaoPaymentGateway( @Value("${payment.kakao.rest-api-key}") String restApiKey, KakaoHttpClient httpClient) { this.restApiKey = restApiKey; this.httpClient = httpClient; } @Override protected PaymentResult doPay(Order order) { // Kakao API调用逻辑 return new PaymentResult("kakao" + System.currentTimeMillis(), "SUCCESS"); } @Override public String getGatewayId() { return "KAKAO"; } }- 修改工厂决策逻辑(仅1行):
private String resolveGatewayId(PaymentContext context) { Region region = context.getRegion(); // ... 其他逻辑 else if (region == Region.SOUTH_KOREA) { return "KAKAO"; // 新增这一行 } // ... }- 添加配置:
# application.yml payment: kakao: enabled: true rest-api-key: your_kakao_key全程不修改任何调用方代码(OrderService),不重启应用(配合Spring Cloud Config可热刷新),这就是工厂模式兑现的承诺。我们线上系统实测:从接到需求到灰度上线,仅用47分钟。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 “Bean not found”异常:Spring找不到你的工厂Bean?
这是新手最高频报错。现象:NoSuchBeanDefinitionException: No qualifying bean of type 'PaymentGatewayFactory' available。
排查路径:
- 检查工厂类是否加了
@Component(或@Service)?漏掉注解是主因; - 检查包扫描路径:
@SpringBootApplication的scanBasePackages是否包含工厂类所在包?默认只扫启动类同包及子包; - 检查是否在
@Configuration类中用@Bean方式定义了同名工厂,导致Spring优先使用配置类里的Bean,而忽略@Component; - 检查是否误用了
@Lazy:@Lazy可能导致工厂Bean在首次调用时才初始化,若初始化失败则报此错。
实操心得:在IDEA中按
Ctrl+Click点击paymentGatewayFactory,看能否跳转到工厂类。如果跳转失败,说明Spring根本没识别到这个Bean,优先检查第1、2步。
5.2 工厂返回null:为什么gatewayMap.get("WECHAT")是null?
明明WechatPaymentGateway类上有@Component,getGatewayId()也返回"WECHAT",但工厂里取出来是null。
根因分析:
WechatPaymentGateway的@Component生效了,但它的getGatewayId()方法在Spring注入gatewayMap时尚未执行!因为gatewayMap是构造器注入,而getGatewayId()是实例方法,需要对象实例化后才能调。- Spring的Map注入机制是:遍历所有
PaymentGateway类型的Bean,调用其getGatewayId()方法,用返回值作key。但如果getGatewayId()方法里有@Value注入的字段(如this.appId),而该字段还未初始化,就会返回null或空字符串。
解决方案:
- 强制初始化:在
WechatPaymentGateway构造器末尾加log.debug("WechatGateway initialized with appId: {}", this.appId);,确认字段已赋值; - 改用静态常量:
public static final String GATEWAY_ID = "WECHAT";,在getGatewayId()中直接返回,避免依赖实例字段; - 最稳妥方案:用
@PostConstruct标注初始化方法,在Spring完成所有字段注入后再设置ID。
我们团队采用第三种,确保万无一失:
public class WechatPaymentGateway extends AbstractPaymentGateway { private String gatewayId; // 不再final @PostConstruct public void init() { this.gatewayId = "WECHAT"; } @Override public String getGatewayId() { return gatewayId; } }5.3 性能瓶颈:工厂方法调用慢,拖垮整个接口?
某次压测发现,PaymentGatewayFactory.create()平均耗时12ms,远超预期。排查后发现是resolveGatewayId()里做了同步远程调用(查配置中心),而工厂方法本应是轻量决策。
优化方案:
- 预加载缓存:在
@PostConstruct中,从配置中心拉取所有渠道开关配置,存入ConcurrentHashMap; - 异步刷新:用
ScheduledExecutorService每隔30秒异步更新缓存,避免阻塞主流程; - 降级策略:缓存失效时,返回默认网关,不抛异常。
改造后,工厂方法P99耗时从12ms降至0.08ms,QPS提升3倍。
5.4 单元测试难题:如何Mock工厂返回指定网关?
很多开发者写测试时,试图Mockito.mock(PaymentGatewayFactory.class),然后when(factory.create(any())).thenReturn(mockGateway)。这会导致两个问题:一是工厂本身有复杂逻辑,Mock后失去测试价值;二是create()方法被Mock,无法验证决策逻辑是否正确。
正确姿势:不Mock工厂,而是Mock其依赖的网关Bean,并在测试配置中只加载需要的Bean。
@SpringBootTest(classes = { TestConfig.class, // 自定义测试配置 WechatPaymentGateway.class, // 只加载这个 PaymentGatewayFactory.class }) class PaymentGatewayFactoryTest { @Autowired private PaymentGatewayFactory factory; @Test void shouldReturnWechatForChinaRegion() { PaymentContext context = PaymentContext.builder() .region(Region.CHINA) .build(); PaymentGateway gateway = factory.create(context); assertThat(gateway).isInstanceOf(WechatPaymentGateway.class); assertThat(gateway.getGatewayId()).isEqualTo("WECHAT"); } }TestConfig中可定义@Bean模拟网关,或直接用@MockBean替换真实网关。关键是让工厂在测试环境中真实运行决策逻辑,这才是单元测试的意义。
5.5 面试高频题实战:工厂模式与IoC容器的关系?
面试官常问:“Spring的IoC容器是不是工厂模式的实现?” 正确回答不是“是”或“不是”,而是分层解释:
- 底层机制相似:Spring的
BeanFactory确实通过反射Class.forName().getDeclaredConstructor().newInstance()创建对象,符合“工厂”行为; - 目标层次不同:IoC解决的是“对象创建和依赖管理”的基础设施问题,工厂模式解决的是“业务上下文驱动的对象选择”问题;
- 协作关系:工厂是IoC容器的“客户”,它从容器中获取依赖(如HTTP Client),再根据业务规则返回特定产品(如WechatGateway);
- 不可替代性:没有IoC,你可以手写工厂;没有工厂,IoC无法满足动态决策需求。
一句话总结:IoC是造砖厂(统一生产标准砖块),工厂是建筑师(根据户型图选用合适砖块搭房子)。两者配合,才能盖出高质量建筑。
6. 最后分享一个血泪教训:别在工厂里做“脏活”
三年前,我在一个金融项目里犯了个致命错误:为了让工厂“更智能”,在create()方法里加入了数据库查询(查用户白名单)、远程调用(调风控服务)、甚至文件IO(读取本地证书)。结果上线后,支付接口平均响应时间飙升至2.3秒,SLO全线告急。
复盘发现:工厂方法被设计成了“瑞士军刀”,承担了本该由Service层处理的业务逻辑。正确的分层应该是:
- Controller层:接收请求,校验参数;
- Service层:编排业务流程,调用风控、查询DB、发消息;
- Factory层:纯决策,基于Service层传入的上下文(如
isHighRiskUser=true)返回对应网关; - Gateway层:专注API调用,不掺杂业务判断。
现在我们团队的红线是:工厂方法内禁止出现@Value以外的任何@Autowired,禁止调用任何Service、Repository、RestTemplate,禁止任何IO操作。所有“脏活”必须前置到Service,工厂只做“if-else”和“return”。
这个教训让我明白:设计模式的价值,不在于炫技,而在于守住边界。当你能把“创建对象”这件事做到极致简单、极致可靠、极致可测,那些复杂的业务逻辑,自然就有了安放之地。