设计模式实战解读(五):策略模式——干掉 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 #函数式编程 #支付策略 #连接器鉴权 #面向对象 #软件工程

相关推荐
李少兄1 小时前
Java 短路求值的优雅实践:用 `&&` 实现安全高效的批量操作控制
java·开发语言
oddsand11 小时前
AI应用开发学习步骤-java
java·人工智能·学习
莫***妞1 小时前
2026年java后端开发还有未来吗? 就业形式如何?
java·开发语言
鱼鳞_1 小时前
苍穹外卖-Day08(购物车)
java·spring boot
wand codemonkey1 小时前
(三十三)【OA系统开发】实战-【开发规范】+【环境配置】
java
nickel3691 小时前
Qoder相关使用
java·开发语言·vue.js·spring boot
两年半的个人练习生^_^1 小时前
Java IO流之BIO
java·开发语言
笨蛋不要掉眼泪1 小时前
Java并发编程:深入剖析 ArrayBlockingQueue
java·开发语言·算法·并发
Refrain_zc1 小时前
Android 封装 BaseMultipleChoiceAdapter 快速实现列表多选编辑
java