Files
2026-09-26 16:09:11 +08:00

6.5 KiB

设计模式应用实践

适当的应用设计模式,能显著提高代码的可读性、维护性。我们应该学习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、示例代码

@RequiredArgsConstructor
public enum LoginType {

    PASSWORD("账号密码登录"),

    SMS("短信验证码登录"),

    EMAIL("邮箱验证码登录");

    private final String description;

}
@Data
public class LoginRequest {

    private LoginType loginType;

    private Object param;

}
public interface IUserLoginService {

    LoginType getLoginType();

    void doLogin(Object param);

}
@Component
public class PasswordLoginService implements IUserLoginService {

    @Override
    public LoginType getLoginType() {
        return LoginType.PASSWORD;
    }

    @Override
    public void doLogin(Object param) {
        // 执行账号密码登录
    }
    
}
@Component
public class SmsLoginService implements IUserLoginService {

    @Override
    public LoginType getLoginType() {
        return LoginType.SMS;
    }

    @Override
    public void doLogin(Object param) {
        // 执行短信验证码登录
    }

}
@Component
public class EmailLoginService implements IUserLoginService {

    @Override
    public LoginType getLoginType() {
        return LoginType.EMAIL;
    }

    @Override
    public void doLogin(Object param) {
        // 执行邮箱验证码登录
    }

}
@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、示例代码

@Data
@RequiredArgsConstructor
public class UserRegisteredEvent {
    private final Long userId;
    private final String username;
    private final String email;
}
@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));
    }

}
@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());
    }
}