设计模式实战解读(五):策略模式——干掉 if-else 的优雅方案

🔔 本文 5000+ 字深度原创,含完整代码示例和生产级落地方案。创作不易,如果对你有帮助,请点赞 👍 收藏 ⭐ 关注 🔥 三连支持,你的认可是我持续输出的最大动力!
本文是「设计模式实战解读」系列第五篇。系列文章统一按照 定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ 的结构展开,每篇聚焦一个模式讲透。


一句话定义

策略模式(Strategy):定义一族算法,把每个算法封装成独立的类,使它们可以互相替换。调用方选择使用哪种策略,不关心策略的内部实现。

归属:行为型模式。


一、没有策略模式时的痛点

假设你在做一个支付模块,需要支持多种支付方式:

java 复制代码
public class PaymentService {

    public PayResult pay(PayRequest request) {
        if ("wechat".equals(request.getPayType())) {
            // 微信支付逻辑(20 行)
            String prepayId = wechatClient.createOrder(...);
            return buildWechatResult(prepayId);

        } else if ("alipay".equals(request.getPayType())) {
            // 支付宝逻辑(25 行)
            String tradeNo = alipayClient.tradeCreate(...);
            return buildAlipayResult(tradeNo);

        } else if ("unionpay".equals(request.getPayType())) {
            // 银联逻辑(30 行)
            String tn = unionPayClient.frontTransReq(...);
            return buildUnionPayResult(tn);

        } else if ("balance".equals(request.getPayType())) {
            // 余额支付逻辑(15 行)
            deductBalance(request.getUserId(), request.getAmount());
            return buildBalanceResult();
        }
        // 随着支付方式增加,这个方法会一直膨胀...
        throw new BizException("Unsupported pay type");
    }
}

这段代码的问题:

  1. 违反开闭原则:每增加一种支付方式,都要修改这个方法
  2. 方法越来越长:4 种支付方式已经 90+ 行,10 种就是 200+ 行
  3. 难以测试:测试某种支付方式,要绕过所有其他分支
  4. 无法复用:微信支付的逻辑散在 if 块里,其他地方想复用也难以抽取
  5. 维护风险高:修改微信支付逻辑时,可能误改 if-else 结构影响其他支付方式

这就是典型的"分支爆炸"------当业务逻辑按照某个**维度(支付类型、折扣类型、导出格式...)**有多种差异化实现时,if-else 就会失控。


二、模式结构

复制代码
┌──────────────────────────────┐
│         Context(上下文)      │
├──────────────────────────────┤
│ - strategy: PayStrategy      │ ← 持有策略引用
├──────────────────────────────┤
│ + setStrategy(PayStrategy)   │
│ + executeStrategy(request)   │ ← 委托给策略执行
└──────────────────────────────┘
              │ 依赖
              ↓
┌──────────────────────────────┐
│       PayStrategy(接口)      │
├──────────────────────────────┤
│ + pay(PayRequest): PayResult │ ← 统一的策略接口
└──────────────────────────────┘
        ↑        ↑        ↑
   WechatPay  Alipay  UnionPay  ← 具体策略实现

三个角色:

  • Strategy(策略接口):定义算法的统一签名
  • ConcreteStrategy(具体策略):每种算法的具体实现
  • Context(上下文):持有策略引用,对外提供调用入口

关键点:Context 不知道具体用了哪个策略,策略对 Context 透明。


三、核心实现

3.1 基础版:接口 + 实现类

java 复制代码
// 策略接口
public interface PayStrategy {
    PayResult pay(PayRequest request);

    // 声明自己支持哪种支付类型(用于策略选择)
    String payType();
}

// 具体策略:微信支付
@Component
public class WechatPayStrategy implements PayStrategy {

    @Override
    public String payType() {
        return "wechat";
    }

    @Override
    public PayResult pay(PayRequest request) {
        // 微信支付完整逻辑,独立封装,互不干扰
        String prepayId = wechatClient.createOrder(
            request.getAmount(), request.getOutTradeNo(), request.getNotifyUrl()
        );
        return PayResult.success(prepayId);
    }
}

// 具体策略:支付宝
@Component
public class AlipayStrategy implements PayStrategy {

    @Override
    public String payType() {
        return "alipay";
    }

    @Override
    public PayResult pay(PayRequest request) {
        String tradeNo = alipayClient.tradeCreate(...);
        return PayResult.success(tradeNo);
    }
}

3.2 Spring 自动注册(推荐)

java 复制代码
// 策略上下文:自动收集所有策略实现
@Component
public class PayContext {

    // Spring 把所有 PayStrategy 实现按 Bean Name 注入
    private final Map<String, PayStrategy> strategyMap;

    public PayContext(List<PayStrategy> strategies) {
        this.strategyMap = strategies.stream()
            .collect(Collectors.toMap(PayStrategy::payType, Function.identity()));
    }

    public PayResult pay(PayRequest request) {
        PayStrategy strategy = strategyMap.get(request.getPayType());
        if (strategy == null) {
            throw new BizException(ErrorCode.PAY_TYPE_NOT_SUPPORTED,
                "Unsupported pay type: " + request.getPayType());
        }
        return strategy.pay(request);
    }
}

改造后的 PaymentService:

java 复制代码
@Service
public class PaymentService {

    @Autowired
    private PayContext payContext;

    public PayResult pay(PayRequest request) {
        // 原来 90 行变成 1 行
        return payContext.pay(request);
    }
}

新增一种支付方式(比如Apple Pay),只需要:

java 复制代码
@Component
public class ApplePayStrategy implements PayStrategy {
    @Override
    public String payType() { return "applepay"; }

    @Override
    public PayResult pay(PayRequest request) { /* ... */ }
}

一行代码都不需要修改。 这就是策略模式的核心价值:让扩展变成"加"而不是"改"。

3.3 函数式策略(Lambda)

当策略逻辑简单时,可以用 Lambda 代替独立类:

java 复制代码
// 策略接口本身就是函数式接口
@FunctionalInterface
public interface DiscountStrategy {
    BigDecimal calculate(BigDecimal originalPrice, int quantity);
}

// 用 Lambda 定义策略,不需要单独建类
public class DiscountStrategyRegistry {

    private static final Map<String, DiscountStrategy> REGISTRY = new HashMap<>();

    static {
        // 九折优惠
        REGISTRY.put("MEMBER_DISCOUNT", (price, qty) -> price.multiply(new BigDecimal("0.9")));
        // 满100减20
        REGISTRY.put("FULL_REDUCTION", (price, qty) -> {
            if (price.compareTo(new BigDecimal("100")) >= 0) {
                return price.subtract(new BigDecimal("20"));
            }
            return price;
        });
        // 阶梯折扣
        REGISTRY.put("LADDER_DISCOUNT", (price, qty) -> {
            if (qty >= 10) return price.multiply(new BigDecimal("0.8"));
            if (qty >= 5)  return price.multiply(new BigDecimal("0.85"));
            return price;
        });
    }

    public static BigDecimal calculate(String discountCode, BigDecimal price, int qty) {
        DiscountStrategy strategy = REGISTRY.getOrDefault(discountCode,
            (p, q) -> p); // 默认不打折
        return strategy.calculate(price, qty);
    }
}

四、真实应用场景

4.1 框架级应用

框架 策略接口 具体策略 策略选择依据
Spring Security AuthenticationProvider DaoAuthenticationProvider / OAuthProvider 认证类型
Spring MVC HandlerAdapter RequestMappingHandlerAdapter / SimpleControllerHandlerAdapter Controller 类型
MyBatis TypeHandler StringTypeHandler / IntegerTypeHandler Java/JDBC 类型
Java 排序 Comparator 各种比较器 排序字段
Spring Retry RetryPolicy SimpleRetryPolicy / ExceptionClassifierRetryPolicy 重试条件

4.2 业务场景

业务 策略维度 典型策略 选择依据
支付 支付渠道 微信/支付宝/银联/余额 用户选择
折扣计算 优惠类型 会员折扣/满减/阶梯折扣 活动配置
消息通知 通知渠道 邮件/短信/钉钉/企微 用户偏好
文件导出 导出格式 Excel/CSV/PDF/HTML 用户选择
风控 风险等级 拒绝/人工审核/放行 风险分数
数据同步 同步模式 全量/增量/差量 数据量和时效

4.3 iPaaS 连接器鉴权策略

在 iPaaS 平台中,不同连接器的鉴权方式完全不同------这是策略模式的天然场景:

java 复制代码
// 鉴权策略接口
public interface AuthStrategy {
    void auth(HttpRequest request, ConnectorConfig config);
    AuthType supportedAuthType();
}

// OAuth2 鉴权策略
@Component
public class OAuth2AuthStrategy implements AuthStrategy {
    @Override
    public AuthType supportedAuthType() { return AuthType.OAUTH2; }

    @Override
    public void auth(HttpRequest request, ConnectorConfig config) {
        String token = tokenCache.getOrRefresh(config.getConnectorId());
        request.addHeader("Authorization", "Bearer " + token);
    }
}

// API Key 鉴权策略
@Component
public class ApiKeyAuthStrategy implements AuthStrategy {
    @Override
    public AuthType supportedAuthType() { return AuthType.API_KEY; }

    @Override
    public void auth(HttpRequest request, ConnectorConfig config) {
        request.addHeader(config.getKeyHeader(), config.getApiKey());
    }
}

// Basic Auth 鉴权策略
@Component
public class BasicAuthStrategy implements AuthStrategy {
    @Override
    public AuthType supportedAuthType() { return AuthType.BASIC; }

    @Override
    public void auth(HttpRequest request, ConnectorConfig config) {
        String credentials = Base64.encode(config.getUsername() + ":" + config.getPassword());
        request.addHeader("Authorization", "Basic " + credentials);
    }
}

连接器执行器中:

java 复制代码
// 自动根据连接器配置的鉴权类型选择策略
authStrategyMap.get(config.getAuthType()).auth(request, config);

新增一种鉴权方式(比如 HMAC 签名),加一个 HmacAuthStrategy 类即可,连接器执行器不需要改动一行。


五、常见变种

5.1 策略 + 工厂

当策略创建本身有复杂逻辑时(需要初始化状态、依赖配置),用工厂来创建策略:

java 复制代码
public class PayStrategyFactory {

    public PayStrategy create(String payType, PayConfig config) {
        return switch (payType) {
            case "wechat" -> new WechatPayStrategy(config.getWechatAppId(), config.getMchId());
            case "alipay" -> new AlipayStrategy(config.getAlipayAppId(), config.getPrivateKey());
            default -> throw new BizException("Unsupported: " + payType);
        };
    }
}

适用场景:策略实例是有状态的、需要按配置初始化,不适合作为 Spring 单例的情况。

5.2 策略链(责任链的变种)

多个策略按顺序执行,直到某个策略处理成功:

java 复制代码
public class FallbackPayContext {

    private final List<PayStrategy> chain; // 按优先级排序

    public PayResult pay(PayRequest request) {
        for (PayStrategy strategy : chain) {
            if (strategy.supports(request)) {
                try {
                    return strategy.pay(request);
                } catch (PayException e) {
                    log.warn("Strategy {} failed, trying next", strategy.payType(), e);
                }
            }
        }
        throw new BizException("All strategies failed");
    }
}

5.3 枚举策略

当策略种类有限且固定时,用枚举简洁实现:

java 复制代码
public enum ShippingStrategy {

    STANDARD {
        @Override
        public BigDecimal calculateFee(int weight, int distance) {
            return new BigDecimal("5").add(new BigDecimal(weight).multiply(new BigDecimal("0.01")));
        }
    },
    EXPRESS {
        @Override
        public BigDecimal calculateFee(int weight, int distance) {
            return new BigDecimal("15").add(new BigDecimal(weight).multiply(new BigDecimal("0.02")));
        }
    };

    public abstract BigDecimal calculateFee(int weight, int distance);
}

// 使用
BigDecimal fee = ShippingStrategy.EXPRESS.calculateFee(500, 100);

六、优缺点

优点 缺点
消灭 if-else,逻辑清晰 策略类数量增加
符合开闭原则(新增策略不改原代码) 调用方需要知道有哪些策略可选
每个策略独立,方便单测 简单场景下引入策略模式过度设计
策略可以动态切换 策略之间如果有共享状态,需要额外处理
支持组合(策略链、策略 + 工厂) 策略选择逻辑本身可能变成新的 if-else

七、避坑指南

坑 1:策略本身有状态(并发问题)

策略通常是 Spring 单例(@Component),如果策略类里有成员变量存储"当前请求的状态",多线程并发时会乱。

原则:策略类必须无状态------请求相关的状态通过参数传入,不存在策略类的成员变量里。

java 复制代码
// ❌ 有状态的策略(并发不安全)
@Component
public class WechatPayStrategy implements PayStrategy {
    private PayRequest currentRequest; // 危险!多线程会乱

    public PayResult pay(PayRequest request) {
        this.currentRequest = request; // 并发问题
        ...
    }
}

// ✓ 无状态策略(正确)
@Component
public class WechatPayStrategy implements PayStrategy {
    public PayResult pay(PayRequest request) {
        // request 是方法参数,线程安全
        ...
    }
}

坑 2:策略选择逻辑本身又变成 if-else

java 复制代码
// ❌ 用 if-else 选择策略,治好了一个 if-else 又来一个
String strategy;
if (order.isMember()) {
    strategy = "MEMBER_DISCOUNT";
} else if (order.getTotal().compareTo(new BigDecimal("100")) >= 0) {
    strategy = "FULL_REDUCTION";
} else {
    strategy = "NO_DISCOUNT";
}

解法 :策略接口加一个 supports(context) 方法,让策略自己说明适用条件:

java 复制代码
public interface DiscountStrategy {
    boolean supports(OrderContext context);  // 策略自己声明适用范围
    BigDecimal calculate(OrderContext context);
}

// 策略选择变成遍历
strategies.stream()
    .filter(s -> s.supports(context))
    .findFirst()
    .orElse(noDiscountStrategy)
    .calculate(context);

坑 3:Context 对象膨胀

随着业务增长,传入策略的 Context 参数越来越大,变成一个包含所有可能字段的"上帝对象"。

解法:为不同策略族定义不同的 Context 接口,策略只接受它需要的最小上下文。

坑 4:过度使用策略模式

两三个 if-else 不需要上策略模式。策略模式的适用信号:

  • 分支数量 ≥ 5 且还会继续增加
  • 每个分支的逻辑 ≥ 10 行
  • 有明确的扩展需求(新渠道、新格式、新算法)

两三个简单分支直接写 if-else 更清晰。


八、常见问题(FAQ)

Q:策略模式和工厂模式有什么区别?

A:工厂模式关注的是"创建什么对象 ",策略模式关注的是"用什么算法执行"。实际项目中经常结合:工厂负责创建策略对象,Context 负责调用策略。

Q:策略模式和状态模式很像,怎么区分?

A:策略模式中,策略是外部传入的 (调用方决定用哪个策略,策略本身不会互相转换)。状态模式中,状态是对象内部的(对象根据当前状态决定行为,状态之间可以互相转换)。简单说:策略是"选择",状态是"转换"。

Q:Spring 里直接用多个 if-else 分支调用不同的 @Service,和策略模式有什么实质区别?

A:如果你的 if-else 分支没有抽象出统一接口,只是调用不同的 Service 方法,那就只是"用 Spring 管理依赖的 if-else",不是策略模式。策略模式的核心是统一接口------所有策略对 Context 表现一致,Context 不知道具体哪个实现被调用。

Q:什么时候该用枚举策略,什么时候该用独立类策略?

A:策略数量固定不会动态扩展 (不需要第三方插件新增策略)→ 枚举;策略可能动态扩展(新加策略不改代码)→ 独立类 + Spring 自动注入。


九、小结

策略模式的核心价值:把"用什么算法"的决策和"如何执行算法"的实现解耦。

三个实践要点:

  1. Spring 项目用 List<Strategy> 自动注入 + Map 路由,零手动注册,新增策略一键生效
  2. 策略类必须无状态------成员变量不存运行时数据,请求上下文通过参数传递
  3. 策略接口加 supports() 方法------让策略自我声明适用范围,消灭策略选择层的 if-else

下一篇我们聊装饰器模式------当你需要给一个对象添加新功能,但又不想修改原始类、也不想用继承时,如何优雅地"包装"它。


标签:#设计模式 #策略模式 #Strategy #行为型模式 #Java #Spring #消灭if-else #函数式编程 #支付策略 #连接器鉴权 #面向对象 #软件工程

相关推荐
wno7043 分钟前
Spring Boot JdbcTemplate配置Druid多数据源
java·spring boot·后端
聊浮游34 分钟前
JAVA2026最新全套学习资料、学习路线
java·开发语言·jvm·mysql·spring·maven·idea
MacroZheng1 小时前
完美替代 Navicat!这款内置 AI 的数据库工具,太香了!
java·后端·mysql
SamDeepThinking1 小时前
从REST到gRPC,一个API选型的思考框架
java·后端·程序员
Zane19941 小时前
接口都能写默认实现了,为什么还需要抽象类
java·后端
未秃头的程序猿1 小时前
读Spring AI源码的方法——不是硬啃,是带着问题去翻
java·后端·spring
用户3126874877202 小时前
别再 Thread.sleep 硬等了!并发工具类到底怎么选?
java
Java内核笔记2 小时前
万字长文剖析 Spring Boot 4 自动配置机制源码:从 @EnableAutoConfiguration 到条件装配
java·后端
evans在进步2 小时前
Spring MVC 核心机制详解:全局异常处理、请求跳转与数据绑定
java·spring·mvc
嘟嘟07172 小时前
TypeScript 工具类型通关:Pick、Omit、Partial、Record 区别与 Omit 等价实现一次讲清
设计模式·面试·typescript