一、为什么需要策略模式?
在企业项目开发中,经常遇到这样的业务:同一个功能,由于业务规则不同,需要有多种实现方式。
例如支付(支付宝、微信、银联)、文件存储(OSS、MinIO、本地)、消息通知(短信、邮件、钉钉),以及本例的邮件发送(QQ、163、Gmail)。
这些业务有一个共同特点:
业务流程相同,但是具体实现不同。
如果不使用设计模式,通常会写出大量 if-else:
public void send(String type) {if ("QQ".equals(type)) {// QQ邮箱发送逻辑} else if ("163".equals(type)) {// 163邮箱发送逻辑} else if ("GMAIL".equals(type)) {// Gmail发送逻辑}
}
随着邮箱类型不断增多,这个方法会越来越庞大,每次新增类型都要修改原有代码,明显违反了开闭原则(对扩展开放,对修改关闭)。这时候就可以使用策略模式。
二、策略模式是什么?
策略模式(Strategy Pattern):
定义一组算法或业务规则,将每种实现分别封装起来,并且可以在运行时动态替换。
以前是一个类里面写所有 if-else 逻辑,使用策略模式后,每个策略负责自己的业务,通过统一接口对外暴露。
三、项目中的策略接口
本项目的 EmailStrategy 就是策略抽象,定义了两个方法:发送邮件和获取邮箱类型。
// EmailStrategy.java
package com.example.email.strategy;import com.example.email.dto.MailRequest;public interface EmailStrategy {void send(MailRequest request);String getType();}
该接口不依赖于任何具体的邮箱实现,只约定行为。
四、不同邮箱实现不同策略
QQ邮箱策略
// QQEmailStrategy.java
package com.example.email.strategy;import com.example.email.dto.MailRequest;
import org.springframework.stereotype.Component;@Component
public class QQEmailStrategy implements EmailStrategy {@Overridepublic void send(MailRequest request) {System.out.println("QQ邮箱发送邮件...");System.out.println("收件人:" + request.getTo());}@Overridepublic String getType() {return "QQ";}}
163邮箱策略
// Email163Strategy.java
package com.example.email.strategy;import com.example.email.dto.MailRequest;
import org.springframework.stereotype.Component;@Component
public class Email163Strategy implements EmailStrategy {@Overridepublic void send(MailRequest request) {System.out.println("163邮箱发送邮件...");}@Overridepublic String getType() {return "163";}}
Gmail策略
// GmailStrategy.java
package com.example.email.strategy;import com.example.email.dto.MailRequest;
import org.springframework.stereotype.Component;@Component
public class GmailStrategy implements EmailStrategy {@Overridepublic void send(MailRequest request) {System.out.println("Gmail发送邮件...");}@Overridepublic String getType() {return "GMAIL";}}
每个策略都是一个独立的 @Component,由 Spring 统一管理。新增邮箱时只需新增一个 EmailStrategy 实现类即可,无需修改任何已有代码。
五、工厂模式:如何选择具体策略
策略接口解决了"每种邮箱如何发送"的问题,但业务调用 service.send(request) 时,系统怎么知道选择哪个策略?
通常配合工厂模式解决。
// EmailStrategyFactory.java
package com.example.email.factory;import com.example.email.strategy.EmailStrategy;
import org.springframework.stereotype.Component;import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;@Component
public class EmailStrategyFactory {private final Map<String, EmailStrategy> strategyMap;public EmailStrategyFactory(List<EmailStrategy> strategies) {this.strategyMap = strategies.stream().collect(Collectors.toMap(EmailStrategy::getType,Function.identity()));}public EmailStrategy getStrategy(String type) {EmailStrategy strategy = strategyMap.get(type);if (strategy == null) {throw new RuntimeException("不支持的邮箱类型:" + type);}return strategy;}}
核心原理:Spring 启动时,会自动将所有 EmailStrategy 实现类注入到 List<EmailStrategy> 中,工厂将其转换为 Map<type, strategy> 结构。
调用时只需:
EmailStrategy strategy = factory.getStrategy("QQ");
strategy.send(request);
业务代码完全不知道具体实现类,只通过接口交互。
六、模板模式:固定发送流程
邮件发送的完整流程是固定的,但具体发送行为是变化的。固定部分适合用模板模式封装。
// AbstractEmailTemplate.java
package com.example.email.service;import com.example.email.dto.MailRequest;public abstract class AbstractEmailTemplate {public final void send(MailRequest request) {validate(request);beforeSend(request);doSend(request);afterSend(request);}protected void validate(MailRequest request) {if (request.getTo() == null || request.getTo().isEmpty()) {throw new RuntimeException("收件人不能为空");}}protected void beforeSend(MailRequest request) {System.out.println("开始发送邮件");}protected abstract void doSend(MailRequest request);protected void afterSend(MailRequest request) {System.out.println("邮件发送结束");}}
send() 方法用 final 修饰,确保子类不能修改流程顺序。doSend() 是抽象方法,由子类实现。
七、服务层:模板 + 策略 + 工厂组合
// EmailSendService.java
package com.example.email.service;import com.example.email.dto.MailRequest;
import com.example.email.factory.EmailStrategyFactory;
import com.example.email.strategy.EmailStrategy;
import org.springframework.stereotype.Service;@Service
public class EmailSendService extends AbstractEmailTemplate {private final EmailStrategyFactory factory;public EmailSendService(EmailStrategyFactory factory) {this.factory = factory;}@Overrideprotected void doSend(MailRequest request) {EmailStrategy strategy = factory.getStrategy(request.getEmailType());strategy.send(request);}}
EmailSendService 继承模板,实现 doSend() —— 内部通过工厂获取对应策略执行发送。三者协作关系如下:
Controller|v
EmailSendService.send(request) ← 模板模式(固定流程)|+--- validate(request) ← 参数校验+--- beforeSend(request) ← 前置日志+--- doSend(request) ← 抽象方法,由子类实现| || v| EmailStrategyFactory ← 工厂模式(策略选择)| || v| QQEmailStrategy / Email163Strategy / GmailStrategy| ← 策略模式(具体发送)+--- afterSend(request) ← 后置处理
八、AOP 日志增强
在 Spring Boot 中还可以通过 AOP 统一增强日志,而无需改动业务代码。
// EmailLogAspect.java
package com.example.email.aspect;import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;@Aspect
@Component
public class EmailLogAspect {@Before("execution(* com.example.email.service.EmailSendService.send(..))")public void before() {System.out.println("记录邮件发送日志");}@AfterReturning("execution(* com.example.email.service.EmailSendService.send(..))")public void after() {System.out.println("邮件发送成功日志");}}
@Before 在方法执行前记录日志,@AfterReturning 在方法成功返回后记录日志。这种横切关注点与业务完全解耦。
九、Controller 层
// EmailController.java
package com.example.email.controller;import com.example.email.dto.MailRequest;
import com.example.email.service.EmailSendService;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/email")
public class EmailController {private final EmailSendService service;public EmailController(EmailSendService service) {this.service = service;}@PostMapping("/send")public String send(@RequestBody MailRequest request) {service.send(request);return "success";}}
请求体示例:
{"emailType": "QQ","to": "test@qq.com","subject": "测试邮件","content": "hello"
}
十、项目完整结构
email-demo
├── pom.xml
└── src/main/java/com/example/email/├── EmailApplication.java # Spring Boot 启动类├── controller/EmailController.java # REST 接口入口├── dto/MailRequest.java # 请求参数 DTO├── strategy/│ ├── EmailStrategy.java # 策略接口(抽象)│ ├── QQEmailStrategy.java # QQ 策略(具体实现)│ ├── Email163Strategy.java # 163 策略│ └── GmailStrategy.java # Gmail 策略├── factory/│ └── EmailStrategyFactory.java # 策略工厂├── service/│ ├── AbstractEmailTemplate.java # 模板模式(固定流程)│ └── EmailSendService.java # 服务(模板+工厂+策略)└── aspect/└── EmailLogAspect.java # AOP 日志切面
十一、策略模式优点总结
| 优点 | 说明 |
|---|---|
| 消除 if-else | factory.getStrategy(type) 替代了多重条件判断 |
| 开闭原则 | 新增邮箱只需新增策略类,无需修改核心业务 |
| 方便测试 | 每个策略独立,可单独测试 |
| 组合灵活 | 可配合模板模式、工厂模式、单例模式、AOP 组合使用 |
十二、适用场景
当业务中出现同一个功能,根据不同条件执行不同逻辑时,就可以考虑策略模式。常见场景包括但不限于:
- 支付渠道:支付宝、微信、银联
- 文件存储:OSS、MinIO、本地
- IoT 协议:MQTT、Modbus、OPC UA
- 消息通知:短信、邮件、微信、钉钉
- 第三方 SDK:海康、大华、腾讯云、阿里云
十三、总结
策略模式的核心思想是把变化的业务逻辑封装成不同策略,通过统一接口调用,让系统具备更好的扩展能力。
在 Spring Boot 企业项目中,策略模式通常与工厂模式(策略选择)、模板模式(流程固定)、单例模式(对象复用)、AOP(横切增强)组合使用,共同构建出高内聚低耦合的架构。邮件发送只是一个入门案例,但其设计思想可以复用到支付、存储、通信、通知等几乎所有业务场景中。
