224 lines
6.5 KiB
Markdown
224 lines
6.5 KiB
Markdown
# 设计模式应用实践
|
|
|
|
适当的应用设计模式,能显著提高代码的可读性、维护性。我们应该学习spring和mybatis框架中的实践,让业务代码变得更加健壮和易读。最常用的设计模式比如:工厂、策略、发布订阅、状态机等。
|
|
|
|
## 1、何时使用策略模式
|
|
|
|
- 当`if`或`switch`分支语句>=3个
|
|
- 算法逻辑复杂
|
|
- 运行时动态切换
|
|
- 多种实现完成同一目标,只是规则不同
|
|
|
|
### 1.1、典型的业务场景
|
|
|
|
- 支付方式:微信、支付宝、银行卡、余额、积分抵扣
|
|
- 优惠/促销计算:满减、折扣、秒杀价、会员价、优惠券
|
|
- 多种登录方式:账号密码、短信验证码、微信授权、人脸识别
|
|
- 数据导出:导出 Excel、CSV、PDF、JSON,格式不同但流程一致
|
|
- 消息通知渠道:短信、邮件、站内信、钉钉、企业微信、Push
|
|
- 运费计算:按重量、按件数、包邮、偏远地区、顺丰/圆通不同计费
|
|
- 风控/校验规则:不同用户等级走不同风控策略
|
|
- 排序/压缩/加密算法:多种算法实现同一接口,运行时选择
|
|
- 任务调度策略:立即执行、延迟执行、定时执行、重试策略
|
|
- 反序列化/序列化:按类型选择不同解析器
|
|
|
|
### 1.2、不适合使用策略模式的场景
|
|
|
|
| 场景 | 原因 |
|
|
|------|------|
|
|
| 分支只有 2~3 个且稳定 | 引入类膨胀,得不偿失 |
|
|
| 分支逻辑只有一两行 | if-else 更直观 |
|
|
| 分支之间高度耦合、共享大量状态 | 拆出去反而更复杂 |
|
|
| 一次性、不会扩展的判断 | 过度设计 |
|
|
|
|
### 1.3、Spring下的实践建议
|
|
|
|
- 策略接口加标识方法,如`getUserType()`,结合枚举更佳,便于工厂用`Map`索引。
|
|
- 每个策略类用`@Component`修饰,由`Spring`注入到工厂。
|
|
- 工厂用构造器注入`List<Strategy>`,根据标识方法初始化一个`Map`,通过标识方法的返回值去获取策略。
|
|
|
|
### 1.4、示例代码
|
|
|
|
```java
|
|
@RequiredArgsConstructor
|
|
public enum LoginType {
|
|
|
|
PASSWORD("账号密码登录"),
|
|
|
|
SMS("短信验证码登录"),
|
|
|
|
EMAIL("邮箱验证码登录");
|
|
|
|
private final String description;
|
|
|
|
}
|
|
```
|
|
|
|
```java
|
|
@Data
|
|
public class LoginRequest {
|
|
|
|
private LoginType loginType;
|
|
|
|
private Object param;
|
|
|
|
}
|
|
```
|
|
|
|
```java
|
|
public interface IUserLoginService {
|
|
|
|
LoginType getLoginType();
|
|
|
|
void doLogin(Object param);
|
|
|
|
}
|
|
```
|
|
|
|
```java
|
|
@Component
|
|
public class PasswordLoginService implements IUserLoginService {
|
|
|
|
@Override
|
|
public LoginType getLoginType() {
|
|
return LoginType.PASSWORD;
|
|
}
|
|
|
|
@Override
|
|
public void doLogin(Object param) {
|
|
// 执行账号密码登录
|
|
}
|
|
|
|
}
|
|
```
|
|
|
|
```java
|
|
@Component
|
|
public class SmsLoginService implements IUserLoginService {
|
|
|
|
@Override
|
|
public LoginType getLoginType() {
|
|
return LoginType.SMS;
|
|
}
|
|
|
|
@Override
|
|
public void doLogin(Object param) {
|
|
// 执行短信验证码登录
|
|
}
|
|
|
|
}
|
|
```
|
|
|
|
```java
|
|
@Component
|
|
public class EmailLoginService implements IUserLoginService {
|
|
|
|
@Override
|
|
public LoginType getLoginType() {
|
|
return LoginType.EMAIL;
|
|
}
|
|
|
|
@Override
|
|
public void doLogin(Object param) {
|
|
// 执行邮箱验证码登录
|
|
}
|
|
|
|
}
|
|
```
|
|
|
|
```java
|
|
@Component
|
|
public class UserLoginFactory {
|
|
|
|
private final EnumMap<LoginType, IUserLoginService> map;
|
|
|
|
public UserLoginFactory(List<IUserLoginService> userLoginServiceList) {
|
|
map = new EnumMap<>(LoginType.class);
|
|
userLoginServiceList.forEach(x -> map.put(x.getLoginType(), x));
|
|
}
|
|
|
|
public void doLogin(LoginRequest request) {
|
|
map.get(request.getLoginType()).doLogin(request.getParam());
|
|
}
|
|
|
|
}
|
|
```
|
|
|
|
## 2、何时使用发布定阅模式
|
|
|
|
- 一个动作触发多个后续操作,如用户注册后,需要发邮件、送积分、写日志,但这些逻辑不应阻塞主流程。
|
|
- 发布方不关心谁处理、如何处理,核心业务只负责“通知”,具体响应逻辑由各订阅方自行定义。
|
|
- 需要并行处理不同工作流,同一消息需要触发多个独立的处理链路。
|
|
- 可容忍最终一致性,发布与处理之间存在延迟,不要求强实时响应。
|
|
- 需要与异构系统/服务通信,不同语言、协议、平台的服务之间通过事件解耦。
|
|
- 新增功能不应修改原有代码,每加一个后续操作,只需新增监听器,核心发布逻辑零改动 。
|
|
|
|
### 2.1、典型业务场景
|
|
|
|
- 用户注册 → 发邮件、发优惠券、初始化积分、写审计日志
|
|
- 订单支付成功 → 扣库存、通知物流、更新用户等级、发送推送
|
|
- 数据变更 → 同步到缓存、搜索引擎、数据仓库
|
|
- 系统监控 → 关键业务事件触发告警
|
|
|
|
### 2.2、Spring下的实践建议
|
|
|
|
Spring 提供了内置的事件机制(`ApplicationEvent` + `ApplicationEventPublisher` + `@EventListener`),本质就是发布订阅模式,无需引入外部消息中间件即可实现轻量级解耦。如果在集群环境下需要跨服务通信,应结合消息队列(如RocketMQ、Kafka)使用。
|
|
|
|
### 2.3、示例代码
|
|
|
|
```java
|
|
@Data
|
|
@RequiredArgsConstructor
|
|
public class UserRegisteredEvent {
|
|
private final Long userId;
|
|
private final String username;
|
|
private final String email;
|
|
}
|
|
```
|
|
|
|
```java
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
public class UserService {
|
|
|
|
private final ApplicationEventPublisher eventPublisher;
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
public void register(String username, String email) {
|
|
// 核心业务:保存用户
|
|
Long userId = saveUser(username, email);
|
|
// 发布事件,不关心谁处理
|
|
eventPublisher.publishEvent(new UserRegisteredEvent(userId, username, email));
|
|
}
|
|
|
|
}
|
|
```
|
|
|
|
```java
|
|
@Slf4j
|
|
@Component
|
|
public class UserEventListener {
|
|
|
|
// 场景1:同步监听,强一致逻辑
|
|
@EventListener
|
|
public void handleSync(UserRegisteredEvent event) {
|
|
log.info("记录注册日志: {}", event.getUsername());
|
|
}
|
|
|
|
// 场景2:异步监听,耗时操作不阻塞主线程
|
|
// 需在启动类加 @EnableAsync
|
|
@Async
|
|
@EventListener
|
|
public void handleAsync(UserRegisteredEvent event) {
|
|
// 发邮件、调第三方,耗时 1s+
|
|
sendWelcomeEmail(event.getEmail());
|
|
}
|
|
|
|
// 场景3:事务提交后监听,确保能查到刚写入的数据
|
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
public void handleAfterCommit(UserRegisteredEvent event) {
|
|
// 此时事务已提交,可以安全查询数据库
|
|
initUserPoints(event.getUserId());
|
|
}
|
|
}
|
|
``` |