策略模式在 Spring 项目中如何落地?
简化版
在 Spring 中落地策略模式,通常是让每个策略实现同一个接口并注册成 Bean,再通过 List<Strategy> 或 Map<String, Strategy> 注入到工厂中,按业务类型选择策略执行。这样新增策略时主流程基本不用改。
详细版
Spring 很适合承载策略模式,因为它可以自动管理策略对象的生命周期和依赖。
常见做法是:
- 定义策略接口,例如
NotifyStrategy; - 每种通知方式实现接口,例如短信、邮件、站内信;
- 每个策略类加
@Component; - 工厂注入所有策略 Bean,构建类型到策略的 Map;
- 业务服务只调用工厂获取策略并执行。
这种做法的优势是:
- 策略对象由容器管理;
- 新增策略只需新增 Bean;
- 策略可以继续注入自己的依赖;
- 主流程保持稳定。
需要注意的是,Spring Bean 默认是单例,策略类不要保存某次请求的临时状态。请求参数应该通过方法入参传递。
完整版教学
一、为什么 Spring 和策略模式很搭
策略模式的一个实际问题是:策略对象从哪里来?
在普通 Java 中,你可能需要手动 new 策略,再放进工厂。Spring 提供了依赖注入和 Bean 管理能力,可以让策略类自然地成为容器中的组件。
这带来几个好处:
- 策略可以自动注入 DAO、远程客户端、配置类;
- 工厂不用关心策略如何创建;
- 新增策略时只要加一个实现类;
- 可以在启动时检查策略是否重复注册。
所以在 Spring 项目里,策略模式常常和依赖注入一起使用。
二、以通知渠道为例设计接口
假设系统支持多种通知渠道:
- 短信;
- 邮件;
- 站内信;
- Webhook。
可以定义统一接口:
public interface NotifyStrategy {
String channel();
void send(NotifyRequest request);
}
channel() 用来标识策略类型,send() 执行具体发送逻辑。
每个策略实现自己的渠道:
@Component
public class SmsNotifyStrategy implements NotifyStrategy {
public String channel() {
return "sms";
}
public void send(NotifyRequest request) {
// 调用短信服务
}
}
三、用 List 注入所有策略
Spring 可以把同一接口的所有 Bean 注入成一个列表:
@Component
public class NotifyStrategyFactory {
private final Map<String, NotifyStrategy> strategyMap;
public NotifyStrategyFactory(List<NotifyStrategy> strategies) {
this.strategyMap = strategies.stream()
.collect(Collectors.toMap(NotifyStrategy::channel, s -> s));
}
public NotifyStrategy get(String channel) {
NotifyStrategy strategy = strategyMap.get(channel);
if (strategy == null) {
throw new IllegalArgumentException("unsupported channel: " + channel);
}
return strategy;
}
}
业务服务使用时:
public void notify(NotifyRequest request) {
NotifyStrategy strategy = factory.get(request.getChannel());
strategy.send(request);
}
主流程非常稳定:拿策略,执行策略。
四、也可以利用 Bean 名称做映射
Spring 还支持直接注入:
private final Map<String, NotifyStrategy> strategyMap;
这个 Map 的 key 默认是 Bean 名称。比如 smsNotifyStrategy。
这种方式少写一个 channel() 方法,但它把业务类型和 Bean 名称绑定在一起。面试中可以说:如果业务码需要清晰稳定,推荐策略自己暴露 type/channel,不要强依赖 Bean 名。
五、Spring 策略模式常见坑
第一,策略 Bean 默认单例。
不要这样写:
private NotifyRequest currentRequest;
如果多个请求并发进来,会产生线程安全问题。请求级数据应该放在方法参数里。
第二,策略选择不要散落。
不要每个 Service 都自己筛选策略,否则后续维护会变复杂。统一工厂更清晰。
第三,异常和默认策略要明确。
未知渠道是报错、忽略、还是走默认通知,要根据业务语义决定,不能随意吞掉。
第四,策略数量多时要考虑可观测性。
比如记录实际命中的策略、耗时、失败原因,排查线上问题会方便很多。
六、容器发现与业务 key 映射
系统有短信、邮件、站内信 3 个 Bean,启动时构建 3 项不可变映射;一次请求只按 channel key 查找并执行目标 Bean。
Spring beans -> collect -> key() uniqueness -> strategyMap -> request lookup
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 容器解决实例装配,不会替你定义 key 冲突、兜底、幂等和业务错误语义。 |
| 适用边界 | Spring Bean 默认单例,因此策略实现应无请求级可变字段;外部资源依赖仍通过构造器注入。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:容器解决实例装配,不会替你定义 key 冲突、兜底、幂等和业务错误语义。
八、常见误区与追问
- 误区:给每个策略加 @Component 就算完整落地。 还需稳定接口、唯一 key、选择器、未知类型处理、监控和测试。
- 误区:用了策略模式,系统里就不该再出现任何条件判断。 策略消除的是反复扩张的业务算法分支;选择策略、校验输入和兜底仍可能需要有限判断。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:为什么推荐构造器注入策略集合? 依赖完整且便于测试,也能在启动阶段统一校验映射。
- 追问:策略对象可以保存本次请求的可变状态吗? 容器单例策略通常不应保存请求状态,应把订单、用户等数据作为参数传入,否则并发调用会相互污染。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
Spring 落地策略模式的关键词是“接口 + 多实现 Bean + 工厂 Map”。策略对象交给容器管理,业务服务只按类型取策略并执行;同时要注意单例无状态、类型映射清晰、异常兜底明确。