策略模式笔记

免费版Java学习笔记(38w字)链接:https://www.yuque.com/aoyouaoyou/sgcqr8

免费版Java面试题(28w字)链接:https://www.yuque.com/aoyouaoyou/wh3hto

完整版可在小红书搜索:遨游qk0

策略模式是行为型设计模式,核心思想是"定义一系列可替换的算法,将每个算法封装为独立类,使算法可独立于使用它的客户端变化",通过上下文类动态选择算法,消除冗余的条件判断(if-else/switch),提高代码扩展性。适用于存在多种方案、需动态切换的场景(如支付方式、排序算法、折扣规则)。

此处为语雀内容卡片,点击链接查看:https://www.yuque.com/aoyouaoyou/pw1w5m/ylswgg47et43441q

此处为语雀内容卡片,点击链接查看:https://www.yuque.com/aoyouaoyou/pbz18g/cho47q57wrh23fg4

代码位置:【demo1的下代码是本节代码;demo2下的代码是面试笔记中策略模式的代码】

一、概念

1. 定义

策略模式(Strategy Pattern)原始定义:定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。策略模式让算法可以独立于使用它的客户端而变化。

2. 生活场景类比

电商平台"商品折扣策略":平台针对不同用户(新用户、会员、老用户)和不同活动(满减、打折、无折扣)提供多种折扣方案。

  • 折扣策略(算法):新用户满100减30、会员9折、老用户无折扣、节日双倍积分;
  • 客户端(用户下单):无需关心折扣计算细节,只需选择对应的用户类型或活动,系统自动匹配折扣策略。
    通过策略模式,每种折扣方案封装为独立策略类,新增折扣(如优惠券抵扣)时无需修改原有代码,直接新增策略类即可。

类似场景还有:出行路线规划(公交、地铁、自驾)、文件压缩算法(ZIP、RAR、7Z)、数据校验规则(手机号校验、邮箱校验)。

3. 开发痛点

  • 条件判断冗余:多个算法通过if-else/switch判断选择,代码臃肿且难以维护;
  • 扩展性差:新增算法需修改原有条件判断代码,违反开闭原则;
  • 算法耦合度高:所有算法集中在一个类中,修改一个算法可能影响其他算法;
  • 可读性低:复杂条件判断和算法逻辑混合,代码难以理解。

二、角色

|------------------------|----------------------------------|-------------------------------------------------|
| 角色名称 | 核心职责 | 示例(折扣策略场景) |
| 抽象策略(Strategy) | 定义算法的统一接口,声明所有具体策略需实现的方法 | 折扣策略接口(DiscountStrategy) |
| 具体策略(ConcreteStrategy) | 实现抽象策略接口,封装具体的算法逻辑 | 新用户折扣(NewUserDiscount)、会员折扣(MemberDiscount) |
| 上下文(Context) | 持有抽象策略的引用,提供接口供客户端设置策略,动态选择并执行算法 | 订单上下文(OrderContext) |

三、模式实现(电商折扣策略场景)

场景说明

实现电商订单折扣计算功能,支持以下策略:

  1. 新用户策略:订单满100减30,不满100无折扣;
  2. 会员策略:订单总价9折,无门槛;
  3. 老用户策略:无折扣,但赠送10积分;
  4. 节日策略:订单满200减50,同时享9.5折(叠加优惠)。
    要求支持动态切换策略,新增策略无需修改原有代码。

UML图和流程图

1. 抽象策略接口

复制代码
import java.math.BigDecimal;

/**
 * 抽象策略:折扣策略接口
 * 定义折扣计算的统一方法
 */
public interface DiscountStrategy {
    // 计算折扣后价格:参数为订单原价,返回折扣后价格
    BigDecimal calculate(BigDecimal originalPrice);

    // 获取策略名称(用于日志输出)
    String getStrategyName();
}

2. 具体策略实现

复制代码
/**
 * 具体策略1:新用户折扣(满100减30)
 */
public class NewUserDiscount implements DiscountStrategy {
    @Override
    public BigDecimal calculate(BigDecimal originalPrice) {
        BigDecimal threshold = new BigDecimal("100");
        BigDecimal discount = new BigDecimal("30");
        if (originalPrice.compareTo(threshold) >= 0) {
            return originalPrice.subtract(discount);
        }
        return originalPrice;
    }

    @Override
    public String getStrategyName() {
        return "新用户满100减30";
    }
}

/**
 * 具体策略2:会员折扣(9折)
 */
public class MemberDiscount implements DiscountStrategy {
    @Override
    public BigDecimal calculate(BigDecimal originalPrice) {
        BigDecimal rate = new BigDecimal("0.9");
        return originalPrice.multiply(rate).setScale(2, BigDecimal.ROUND_HALF_UP);
    }

    @Override
    public String getStrategyName() {
        return "会员9折";
    }
}

/**
 * 具体策略3:老用户折扣(无折扣,送10积分)
 */
public class OldUserDiscount implements DiscountStrategy {
    @Override
    public BigDecimal calculate(BigDecimal originalPrice) {
        System.out.println("老用户福利:赠送10积分");
        return originalPrice;
    }

    @Override
    public String getStrategyName() {
        return "老用户无折扣送积分";
    }
}

/**
 * 具体策略4:节日折扣(满200减50+9.5折)
 */
public class FestivalDiscount implements DiscountStrategy {
    @Override
    public BigDecimal calculate(BigDecimal originalPrice) {
        // 第一步:满200减50
        BigDecimal threshold = new BigDecimal("200");
        BigDecimal subtract = new BigDecimal("50");
        BigDecimal priceAfterSubtract = originalPrice.compareTo(threshold) >= 0 
                ? originalPrice.subtract(subtract) : originalPrice;
        
        // 第二步:9.5折
        BigDecimal rate = new BigDecimal("0.95");
        return priceAfterSubtract.multiply(rate).setScale(2, BigDecimal.ROUND_HALF_UP);
    }

    @Override
    public String getStrategyName() {
        return "节日满200减50+9.5折";
    }
}

3. 策略工厂(消除条件判断)

复制代码
import java.util.HashMap;
import java.util.Map;

/**
 * 策略工厂:管理所有策略,根据策略类型获取对应策略(消除if-else)
 */
public class DiscountStrategyFactory {
    // 存储策略:key为策略类型,value为策略对象
    private static final Map<String, DiscountStrategy> STRATEGY_MAP = new HashMap<>();

    // 静态初始化策略(也可通过配置文件+反射初始化,完全符合开闭原则)
    static {
        STRATEGY_MAP.put("NEW_USER", new NewUserDiscount());
        STRATEGY_MAP.put("MEMBER", new MemberDiscount());
        STRATEGY_MAP.put("OLD_USER", new OldUserDiscount());
        STRATEGY_MAP.put("FESTIVAL", new FestivalDiscount());
    }

    // 私有构造器,防止实例化
    private DiscountStrategyFactory() {}

    // 根据策略类型获取策略
    public static DiscountStrategy getStrategy(String strategyType) {
        DiscountStrategy strategy = STRATEGY_MAP.get(strategyType);
        if (strategy == null) {
            throw new IllegalArgumentException("未知的折扣策略类型:" + strategyType);
        }
        return strategy;
    }

    // 新增策略(扩展方法,无需修改原有逻辑)
    public static void addStrategy(String strategyType, DiscountStrategy strategy) {
        STRATEGY_MAP.put(strategyType, strategy);
    }
}

4. 上下文类(使用策略)

复制代码
import java.math.BigDecimal;

/**
 * 上下文类:订单上下文,持有策略引用,提供策略执行入口
 */
public class OrderContext {
    // 持有抽象策略引用
    private DiscountStrategy discountStrategy;

    // 构造方法注入策略(也可通过setter动态切换)
    public OrderContext(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    // 动态切换策略
    public void setDiscountStrategy(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    // 执行策略:计算最终订单价格
    public BigDecimal calculateFinalPrice(BigDecimal originalPrice) {
        System.out.println("当前使用策略:" + discountStrategy.getStrategyName());
        return discountStrategy.calculate(originalPrice);
    }
}

5. 客户端测试

复制代码
import java.math.BigDecimal;

/**
 * 客户端:测试不同折扣策略的使用
 */
public class StrategyClient {
    public static void main(String[] args) {
        // 订单原价:200元
        BigDecimal originalPrice = new BigDecimal("200");

        // 1. 使用新用户策略
        DiscountStrategy newUserStrategy = DiscountStrategyFactory.getStrategy("NEW_USER");
        OrderContext order1 = new OrderContext(newUserStrategy);
        BigDecimal finalPrice1 = order1.calculateFinalPrice(originalPrice);
        System.out.println("新用户最终价格:" + finalPrice1 + "元\n");

        // 2. 切换为会员策略
        DiscountStrategy memberStrategy = DiscountStrategyFactory.getStrategy("MEMBER");
        order1.setDiscountStrategy(memberStrategy);
        BigDecimal finalPrice2 = order1.calculateFinalPrice(originalPrice);
        System.out.println("会员最终价格:" + finalPrice2 + "元\n");

        // 3. 使用节日策略
        DiscountStrategy festivalStrategy = DiscountStrategyFactory.getStrategy("FESTIVAL");
        OrderContext order2 = new OrderContext(festivalStrategy);
        BigDecimal finalPrice3 = order2.calculateFinalPrice(originalPrice);
        System.out.println("节日最终价格:" + finalPrice3 + "元\n");

        // 4. 新增策略:优惠券抵扣(无需修改原有代码)
        DiscountStrategy couponStrategy = new CouponDiscount(new BigDecimal("20"));
        DiscountStrategyFactory.addStrategy("COUPON", couponStrategy);
        DiscountStrategy newStrategy = DiscountStrategyFactory.getStrategy("COUPON");
        OrderContext order3 = new OrderContext(newStrategy);
        BigDecimal finalPrice4 = order3.calculateFinalPrice(originalPrice);
        System.out.println("优惠券抵扣后价格:" + finalPrice4 + "元");
    }

    // 新增具体策略:优惠券抵扣(满100可用,抵扣固定金额)
    static class CouponDiscount implements DiscountStrategy {
        private BigDecimal couponAmount;

        public CouponDiscount(BigDecimal couponAmount) {
            this.couponAmount = couponAmount;
        }

        @Override
        public BigDecimal calculate(BigDecimal originalPrice) {
            BigDecimal threshold = new BigDecimal("100");
            if (originalPrice.compareTo(threshold) >= 0) {
                return originalPrice.subtract(couponAmount);
            }
            return originalPrice;
        }

        @Override
        public String getStrategyName() {
            return "优惠券抵扣" + couponAmount + "元";
        }
    }
}

6. 输出结果

复制代码
当前使用策略:新用户满100减30
新用户最终价格:170元

当前使用策略:会员9折
会员最终价格:180.00元

当前使用策略:节日满200减50+9.5折
节日最终价格:142.50元

当前使用策略:优惠券抵扣20元
优惠券抵扣后价格:180元

四、策略模式的经典应用(框架源码)

1. JDK中的Comparator接口

Comparator是抽象策略,不同的比较逻辑是具体策略,Collections.sort()是上下文,通过传入不同的Comparator实现动态切换排序算法:

复制代码
List<String> list = Arrays.asList("apple", "banana", "orange");
// 策略1:按长度升序
Collections.sort(list, Comparator.comparingInt(String::length));
// 策略2:按字母降序
Collections.sort(list, Comparator.reverseOrder());

2. Spring中的Resource加载策略

Spring的Resource接口定义了资源加载的抽象策略,ClassPathResourceFileSystemResourceUrlResource是具体策略,ResourceLoader是上下文,根据资源路径动态选择加载策略:

复制代码
ResourceLoader loader = new DefaultResourceLoader();
// 策略1:加载类路径资源
Resource classPathResource = loader.getResource("classpath:application.yml");
// 策略2:加载文件系统资源
Resource fileResource = loader.getResource("file:/data/config.yml");

3. 支付框架中的支付策略(如PayJS、YeePay)

支付框架定义统一的PaymentStrategy接口,微信支付、支付宝支付、银联支付是具体策略,上下文类PaymentContext根据支付类型选择对应的策略,客户端无需关心支付细节。

五、模式优缺点与适用场景

1. 优点

  • 消除条件判断:用策略工厂和上下文替代if-else/switch,代码更简洁;
  • 扩展性好:新增算法只需新增策略类,无需修改原有代码,符合开闭原则;
  • 算法解耦:每个算法封装为独立类,职责单一,便于维护和测试;
  • 动态切换:客户端可通过上下文动态切换策略,适应不同业务场景。

2. 缺点

  • 客户端需了解策略:客户端需知道所有策略的存在,才能选择合适的策略;
  • 策略类数量增加:每个算法对应一个策略类,可能导致类数量增多;
  • 上下文与策略耦合:上下文持有策略引用,若策略需依赖其他组件,可能增加配置复杂度。

3. 适用场景

  • 多种算法可选:如排序、加密、压缩、支付方式、折扣规则;
  • 消除条件判断:代码中存在大量if-else/switch判断不同逻辑;
  • 算法需动态切换:如根据用户类型、环境配置选择不同策略;
  • 扩展性要求高:需频繁新增或修改算法,且不希望影响原有代码。

4. 不适用场景

  • 算法极少变化:仅1-2种算法,且后续不会扩展;
  • 算法逻辑简单:无需封装为独立类(如简单的数值计算);
  • 客户端需直接依赖算法细节:客户端需知道算法的内部实现。

六、与相关模式区别

|--------|----------------------------|-------------------|
| 模式名称 | 核心区别 | 适用场景 |
| 策略模式 | 封装可替换的算法,动态切换,基于接口组合 | 多种算法可选(如支付、排序) |
| 模板方法模式 | 父类定义流程骨架,子类实现步骤,基于继承 | 流程固定、步骤可变(如下单、审批) |
| 状态模式 | 封装对象状态,状态变化时自动切换行为,策略由状态决定 | 状态驱动的行为变化(如订单状态) |
| 工厂方法模式 | 封装对象创建,子类决定创建哪种对象,侧重对象创建 | 复杂对象创建(如不同类型产品) |

七、总结

  1. 策略模式的核心是"算法封装+动态选择",通过抽象策略接口、具体策略类、上下文类和策略工厂,消除条件判断,提高扩展性;
  2. 关键设计:抽象策略定义统一接口,具体策略实现算法,策略工厂管理策略实例,上下文类持有策略并提供执行入口;
  3. 经典应用:JDK的Comparator、Spring的ResourceLoader、支付框架的支付策略,核心是"算法解耦+动态切换";
  4. 实践建议:结合策略工厂(Map存储策略)彻底消除if-else;通过反射或配置文件初始化策略,符合开闭原则;策略类若无状态,可复用实例(享元模式优化)。
相关推荐
睡美人的小仙女12710 小时前
Threejs加载环境贴图报错Bad File Format: bad initial token
开发语言·javascript·redis
rayufo10 小时前
【工具】列出指定文件夹下所有的目录和文件
开发语言·前端·python
RANCE_atttackkk10 小时前
[Java]实现使用邮箱找回密码的功能
java·开发语言·前端·spring boot·intellij-idea·idea
缺点内向11 小时前
C#编程实战:如何为Word文档添加背景色或背景图片
开发语言·c#·自动化·word·.net
一起养小猫11 小时前
Flutter for OpenHarmony 实战:记账应用数据统计与可视化
开发语言·jvm·数据库·flutter·信息可视化·harmonyos
zhougl99611 小时前
Java 所有关键字及规范分类
java·开发语言
java1234_小锋12 小时前
Java高频面试题:MyISAM索引与InnoDB索引的区别?
java·开发语言
2501_9445255412 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 支出分析页面
android·开发语言·前端·javascript·flutter
qq_4171292512 小时前
C++中的桥接模式变体
开发语言·c++·算法