在 Java 项目从单体走向多人协作的过程中,Java 设计模式往往是代码可维护性的分水岭。很多团队不是不会写业务,而是对象创建散落各处、条件分支层层嵌套,导致一次需求变更要改动十几个类,回归测试范围被无限放大。本文聚焦两个高频且易落地的模式——工厂模式与策略模式,用可直接复制的生产级代码,讲清它们各自解决什么问题、何时该用、又该如何组合。吃透这两个,你就掌握了”创建”与”行为”两套解耦思路,足以应付日常开发中八成以上的抽象需求。
一、工厂模式:把”new”集中起来
当一类对象的创建逻辑依赖外部配置或环境变量时,散落的 new 会让调用方被迫了解实现细节。更隐蔽的问题是,调用方为了创建一个对象,必须先 import 一堆具体实现类,编译期耦合让模块根本拆不开。工厂模式的核心价值,是把”创建什么”的决策上收到一个地方,让上层只关心”用它”而非”怎么造它”。
1.1 简单工厂的局限
简单工厂用一个 if-else 返回不同子类,但当类型增多,这个类会膨胀成”上帝方法”,每加一种产品都要改它,直接违反开闭原则。更稳妥的做法是升级为工厂方法,让扩展只发生在新增类上,老代码一行都不用动。
1.2 工厂方法实战
下面以”通知发送器”为例:短信、邮件、站内信都实现同一接口,由各自的工厂负责创建,调用方只依赖工厂接口,对具体实现一无所知。
// 统一产品接口
public interface Notifier {
void send(String message);
}
// 具体产品
public class SmsNotifier implements Notifier {
public void send(String message) {
System.out.println("SMS: " + message);
}
}
// 抽象工厂
public interface NotifierFactory {
Notifier create();
}
// 具体工厂
public class SmsNotifierFactory implements NotifierFactory {
public Notifier create() { return new SmsNotifier(); }
}
// 调用方只依赖工厂,不关心具体实现
NotifierFactory factory = new SmsNotifierFactory();
Notifier notifier = factory.create();
notifier.send("订单已发货");
1.3 何时才该上工厂
工厂不是免费午餐。出现以下信号时再引入:对象创建需要读取配置(如从 yml 读渠道类型)、构造过程复杂(要拼装多个依赖或做参数校验)、同一接口有多种实现且会持续增加。反之,创建一个无状态、无依赖的普通对象,直接 new 反而最清晰——不要为了”显得专业”而套工厂。
二、策略模式:让算法可插拔
策略模式解决的是”同一行为有多种算法、且需要运行时切换”的问题。它与工厂模式常被一起使用:工厂负责造出策略对象,策略负责执行具体行为,二者职责分明。需要提醒的是,策略模式最容易被误用的是”为了用而用”。它的前提是有至少两种可互换算法,且调用方不关心选了哪一个;如果只有一种实现,一个普通方法就够,没必要先定义接口再写实现类,那只是把简单问题包装成框架。
2.1 定义策略接口
以”价格优惠计算”为例,满减、折扣、拼团是三种可以互相替换的算法,统一抽象成策略接口,调用方面向接口编程。
public interface DiscountStrategy {
BigDecimal calc(BigDecimal price);
}
public class FullReductionStrategy implements DiscountStrategy {
public BigDecimal calc(BigDecimal price) {
return price.compareTo(new BigDecimal("200")) >= 0
? price.subtract(new BigDecimal("30")) : price;
}
}
// 上下文:持有策略,运行时注入
public class PriceContext {
private DiscountStrategy strategy;
public void setStrategy(DiscountStrategy s) { this.strategy = s; }
public BigDecimal finalPrice(BigDecimal price) {
return strategy.calc(price);
}
}
2.2 与 Spring 结合
在 Spring 项目中,可把全部策略以 Map<String, DiscountStrategy> 形式自动注入,按业务码路由,彻底干掉 switch 满天飞的分支,新增算法只需加一个 @Component 类。
@Component
public class StrategyRouter {
@Autowired
private Map strategies;
public BigDecimal apply(String code, BigDecimal price) {
DiscountStrategy s = strategies.get(code);
if (s == null) throw new IllegalArgumentException("未知策略:" + code);
return s.calc(price);
}
}
2.3 与工厂联手:一个完整闭环
以”下单优惠”接口为例:工厂根据配置中心下发的 strategyCode 造出对应 DiscountStrategy,PriceContext 注入后计算。配置热更新时,只需工厂返回新策略,上下文与业务代码零改动。这种”创建归工厂、执行归策略”的分工,让灰度、回滚、A/B 都只发生在边界处,主流程始终稳定。在高并发读多写少的接口中,用策略模式切换不同的 Redis 缓存方案,也是同样的思路,既能灰度又能回滚,比直接改主流程安全得多。
三、工厂模式 vs 策略模式:一张表看懂
| 维度 | 工厂模式 | 策略模式 |
|---|---|---|
| 关注点 | 对象如何创建 | 行为如何执行 |
| 变化时机 | 类型增减时改工厂 | 算法替换时扩展 |
| 典型场景 | 多数据源、多通道通知 | 优惠计算、校验规则 |
| 协作关系 | 常作为策略的”制造者” | 被工厂造出后注入上下文 |
四、重构信号:你的代码在求救
设计模式是”闻到坏味道才用”,不是”先想着用”。出现以下信号,就是工厂与策略登场的时刻:
- 类型分支反复出现:同一个
if (type.equals("sms"))在发送、日志、统计里各写一遍,改一处要全局搜。 - 一个类里十几个 new:构造逻辑散落,单测要 mock 一长串依赖才能跑起来。
- 新增一种类型要改 N 处:每加一个渠道,从工厂到调用方全都要动,说明创建没收口。
- 测试困难:因为行为被写死在主流程,无法在不启真实依赖的情况下验证不同分支。
下面这段就是典型坏味道——用策略 + 工厂改完之后,新增算法只需加一个类,主流程一行不动:
// 坏味道:行为写死在主流程
public BigDecimal checkout(String type, BigDecimal price) {
if ("full".equals(type)) { /* 满减逻辑 */ }
else if ("discount".equals(type)) { /* 折扣逻辑 */ }
else if ("group".equals(type)) { /* 拼团逻辑 */ }
return price; // 每加一种都要改这里
}
// 改后:策略 + 工厂,主流程零改动
public BigDecimal checkout(DiscountStrategy s, BigDecimal price) {
return s.calc(price);
}
五、落地时的三个注意点
1. 不要为”可能用到”而设计。 只有一个实现时,直接 new 更简单,过度抽象反而增加阅读与维护成本,也让新人理解门槛变高。
2. 命名要暴露意图。 AbstractHandlerFactoryManager 这种名字会逼死同事,宁可叫 OrderNotifierFactory,一眼看懂职责,后期接手的人会感谢你。
3. 策略枚举化。 固定几种算法时,用 enum 实现接口比建一堆类更轻量,编译期就能发现遗漏的分支,避免运行时才爆出”未知策略”。
设计模式不是炫技,而是把”变化点”提前隔离的工程纪律。当你发现自己在不停复制粘贴 if (type.equals("sms")) 这类分支时,就是工厂与策略模式登场的时候。先把”创建”和”行为”这两件事拆干净,后续的需求变更会轻松一个量级,代码也会从”能跑”走向”好改”。




