# 设计模式应用实践 适当的应用设计模式,能显著提高代码的可读性、维护性。我们应该学习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`,根据标识方法初始化一个`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 map; public UserLoginFactory(List 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()); } } ```