news 2026/7/2 19:28:32

Java工厂模式实战:解耦对象创建与业务逻辑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java工厂模式实战:解耦对象创建与业务逻辑

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()抽象方法;再让WechatPaymentFactoryAlipayPaymentFactory各自实现。这样,当需要新增云闪付渠道时,你只需新增一个UnionPayPaymentFactory类,完全不用动原有工厂基类——真正实现了“对扩展开放,对修改关闭”。但要注意:它解决的是单一产品族内不同实现的创建问题,比如所有支付网关都是PaymentGateway接口的实现,但不适用于同时创建“支付网关+风控策略+对账服务”这一整套组合。

  • 抽象工厂模式(Abstract Factory Pattern):这是工厂方法的“升级版集群”。它面向的是产品族(Product Family)的创建。比如你有一套面向国内用户的支付体系(微信网关+本地风控+人民币对账),另一套面向东南亚用户的支付体系(GrabPay网关+跨境风控+多币种对账)。抽象工厂定义了createGateway()createRiskControl()createReconciliation()三个方法,而ChinaPaymentFactorySoutheastAsiaPaymentFactory分别提供各自体系下的全套实现。它的优势在于:保证同一产品族内的对象能协同工作(比如GrabPay网关天然适配其配套风控规则),但代价是结构复杂、类数量爆炸。我们做过测算:当产品族超过3个、每个族内产品类型超4个时,抽象工厂带来的可维护性提升才明显大于其理解成本。所以我的建议是:除非你明确需要跨环境/跨地域/跨版本的整套能力打包,否则优先用工厂方法

提示:别被UML图吓住。判断该用哪种工厂,只问一个问题:“我新增一种产品,是否需要修改现有工厂类?” 如果答案是“是”,那当前方案大概率错了——简单工厂必然失败,工厂方法可能勉强撑住,抽象工厂则需审视是否过度设计。

2.2 为什么Spring的BeanFactory不算“工厂模式”的直接实现?

很多Java开发者看到Spring的BeanFactoryApplicationContext就以为“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接口,ConcreteStrategyAConcreteStrategyB实现它,客户端持有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,否则走本地渠道)决定支付网关;
  • 每个网关需初始化:微信需appIdmchId;Stripe需secretKey;GrabPay需clientIdclientSecret
  • 后续要快速接入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,按工厂模式,只需三步:

  1. 新增产品实现类
@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. 修改工厂决策逻辑(仅1行):
private String resolveGatewayId(PaymentContext context) { Region region = context.getRegion(); // ... 其他逻辑 else if (region == Region.SOUTH_KOREA) { return "KAKAO"; // 新增这一行 } // ... }
  1. 添加配置
# 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

排查路径

  1. 检查工厂类是否加了@Component(或@Service)?漏掉注解是主因;
  2. 检查包扫描路径:@SpringBootApplicationscanBasePackages是否包含工厂类所在包?默认只扫启动类同包及子包;
  3. 检查是否在@Configuration类中用@Bean方式定义了同名工厂,导致Spring优先使用配置类里的Bean,而忽略@Component
  4. 检查是否误用了@Lazy@Lazy可能导致工厂Bean在首次调用时才初始化,若初始化失败则报此错。

实操心得:在IDEA中按Ctrl+Click点击paymentGatewayFactory,看能否跳转到工厂类。如果跳转失败,说明Spring根本没识别到这个Bean,优先检查第1、2步。

5.2 工厂返回null:为什么gatewayMap.get("WECHAT")是null?

明明WechatPaymentGateway类上有@ComponentgetGatewayId()也返回"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”。

这个教训让我明白:设计模式的价值,不在于炫技,而在于守住边界。当你能把“创建对象”这件事做到极致简单、极致可靠、极致可测,那些复杂的业务逻辑,自然就有了安放之地。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/2 19:27:33

如何轻松解锁加密音乐文件:浏览器中的终极音乐格式转换工具

如何轻松解锁加密音乐文件&#xff1a;浏览器中的终极音乐格式转换工具 【免费下载链接】unlock-music 在浏览器中解锁加密的音乐文件。原仓库&#xff1a; 1. https://github.com/unlock-music/unlock-music &#xff1b;2. https://git.unlock-music.dev/um/web 项目地址: …

作者头像 李华
网站建设 2026/7/2 19:23:04

LV30条码扫描器与PIC18F46K40微控制器的嵌入式解决方案

1. LV30条码扫描器与PIC18F46K40微控制器的组合价值在工业自动化和零售管理领域&#xff0c;条码识别系统的可靠性和适应性直接决定了整个业务流程的效率。LV30作为一款工业级条码扫描器&#xff0c;与Microchip的PIC18F46K40微控制器组合&#xff0c;形成了一个兼具灵活性和稳…

作者头像 李华
网站建设 2026/7/2 19:21:27

动作游戏相机计算插值跟随

我们在设计第三人称动作游戏时&#xff0c;会开发相机跟随功能&#xff0c;我们可以直接通过固定人物和相机的距离来每帧设置相机的位置&#xff0c;也就是直接将相机瞬移&#xff0c;也可以通过插值Vector3.Lerp(a, b, t)的方式使相机平滑移动&#xff0c;a代表当前位置&#…

作者头像 李华
网站建设 2026/7/2 19:15:13

企业级AI编排:安全可控的AI能力调度协议

1. 项目概述&#xff1a;当企业级集成遇上大模型&#xff0c;为什么“拼积木”式AI落地正在失效&#xff1f;我在金融行业做系统集成顾问整整十二年&#xff0c;从最早的SOAP WebService手写WSDL文档&#xff0c;到后来用MuleSoft搭API网关&#xff0c;再到去年开始被客户拉着一…

作者头像 李华
网站建设 2026/7/2 19:13:24

Anthropic提示工程层归零:metadata驱动的AI应用新范式

1. 项目概述&#xff1a;这不是一次普通更新&#xff0c;而是一次架构级“蒸发”“Anthropic Just Shipped the Layer That’s Already Going to Zero”——这个标题一出来&#xff0c;我在 Slack 上看到好几个做 LLM 应用架构的同行直接暂停了手头的 PR&#xff0c;截图发到技…

作者头像 李华