设计模式-策略模式

89 阅读15分钟

写在前面

Hello,我是易元,这篇文章是我学习设计模式时的笔记和心得体会。如果其中有错误,欢迎大家留言指正!


背景

随着电商行业竞争的日益激烈,各种促销活动成为吸引用户的重要手段,试想,我们需要开发一个灵活的促销策略系统,以便商户能够快速配置和调整各种促销规则,具体策略如下:

  1. 固定金额折扣,例如满100减20等。
  2. 百分比折扣,例如特殊商品开启八折优惠。
  3. 满减折扣,例如满300减50、满500减100等。

这是一个典型的策略选择问题,而且随着时间的增加,策略还会出现更多类型,且更为复杂。 首先使用常规的编码方式进行功能的实现(使用大量的条件判断语句),后再使用策略模式进行重构,比较两者差距,清晰的理解策略模式,并能够合理使用。

传统编码实现

订单类
public class Order {

    private BigDecimal totalAmount;

    private PromotionTypeEnum promotionType;

    public Order(BigDecimal totalAmount, PromotionTypeEnum promotionType) {
        this.totalAmount = totalAmount;
        this.promotionType = promotionType;
    }

    public BigDecimal getTotalAmount() {
        return totalAmount;
    }

    public void setTotalAmount(BigDecimal totalAmount) {
        this.totalAmount = totalAmount;
    }

    public PromotionTypeEnum getPromotionType() {
        return promotionType;
    }

    public void setPromotionType(PromotionTypeEnum promotionType) {
        this.promotionType = promotionType;
    }
}
  • 订单类,包含订单总金额和促销类型,构造函数及get、set方法。
促销服务类
public class PromotionService {

    public BigDecimal calculateDiscountedPrice(Order order) {

        BigDecimal totalAmount = order.getTotalAmount();

        PromotionTypeEnum promotionType = order.getPromotionType();

        // 固定金额折扣:满100减20
        if (PromotionTypeEnum.FIXED_AMOUNT == promotionType) {
            if (totalAmount.compareTo(new BigDecimal("100")) >= 0) {
                return totalAmount.subtract(new BigDecimal("20"));
            }
        }

        // 百分比折扣:打8折
        else if (PromotionTypeEnum.PERCENTAGE == promotionType) {
            return totalAmount.multiply(new BigDecimal("0.8"));
        }

        // 满减折扣:满300减50,满500减100
        else if (PromotionTypeEnum.THRESHOLD == promotionType) {
            if (totalAmount.compareTo(new BigDecimal("500")) >= 0) {
                return totalAmount.subtract(new BigDecimal("100"));
            }
            else if (totalAmount.compareTo(new BigDecimal("300")) >= 0) {
                return totalAmount.subtract(new BigDecimal("50"));
            }
        }

        return totalAmount;
    }

}
  • 促销服务类,计算折扣后的金额,calculateDiscountedPrice(Order order)方法内使用了大量条件判断语句来分辨使用哪个折扣逻辑,之后返回。
促销类型枚举
@Getter
@AllArgsConstructor
public enum PromotionTypeEnum {

    FIXED_AMOUNT(1"固定金额折扣"),

    PERCENTAGE(2"百分比折扣"),

    THRESHOLD(3"满减折扣"),

    ;

    /**
     * 类型
     */
    private final Integer value;

    /**
     * 策略名称
     */
    private final String name;

    public static String getPromotionName(PromotionTypeEnum typeEnum) {
        PromotionTypeEnum[] values = PromotionTypeEnum.values();
        for (PromotionTypeEnum orderStateEnum : values) {
            if (orderStateEnum == typeEnum) {
                return typeEnum.getName();
            }
        }

        return "未知策略";
    }
}
测试类
@Test
public void test_calculateDiscountedPrice() {

    PromotionService promotionService = new PromotionService();

    Order order1 = new Order(new BigDecimal("150"), PromotionTypeEnum.FIXED_AMOUNT);
    Order order2 = new Order(new BigDecimal("200"), PromotionTypeEnum.PERCENTAGE);
    Order order3 = new Order(new BigDecimal("350"), PromotionTypeEnum.THRESHOLD);
    Order order4 = new Order(new BigDecimal("600"), PromotionTypeEnum.THRESHOLD);

    System.out.println("订单 1 折扣后价格: " + promotionService.calculateDiscountedPrice(order1).toString());
    System.out.println("订单 2 折扣后价格: " + promotionService.calculateDiscountedPrice(order2).toString());
    System.out.println("订单 3 折扣后价格: " + promotionService.calculateDiscountedPrice(order3).toString());
    System.out.println("订单 4 折扣后价格: " + promotionService.calculateDiscountedPrice(order4).toString());
}
  • 测试类,测试促销逻辑是否正确,一次创建了三种不同促销类型的订单,并使用 calculateDiscountedPrice(Order order) 方法计算折扣后的价格。
运行结果
订单 1 折扣后价格: 130
订单 2 折扣后价格: 160.0
订单 3 折扣后价格: 300
订单 4 折扣后价格: 500

Process finished with exit code 0

实现分析

在传统实现种,使用了条件判断语句来处理不同的促销策略,这种方式简单直接,容易理解,但存在一些潜在问题:

1.违反开闭原则

具体表现:当需要添加新的促销策略时,必须修改 PromotionService 类的 calculateDiscountedPrice 方法,添加新的条件判断分支。 例如:假设运营团队要求增加一个"第二件半价"的促销策略,则需要增加如下代码:

public BigDecimal calculateDiscountedPrice(Order order){

 BigDecimal totalAmount = order.getTotalAmount();
 String promotionType = order.getPromotionType();
 
   // 固定金额折扣:满100减20
        if (PromotionTypeEnum.FIXED_AMOUNT == promotionType) {
            if (totalAmount.compareTo(new BigDecimal("100")) >= 0) {
                return totalAmount.subtract(new BigDecimal("20"));
            }
        }

        // 百分比折扣:打8折
        else if (PromotionTypeEnum.PERCENTAGE == promotionType) {
            return totalAmount.multiply(new BigDecimal("0.8"));
        }

        // 满减折扣:满300减50,满500减100
        else if (PromotionTypeEnum.THRESHOLD == promotionType) {
            if (totalAmount.compareTo(new BigDecimal("500")) >= 0) {
                return totalAmount.subtract(new BigDecimal("100"));
            }
            else if (totalAmount.compareTo(new BigDecimal("300")) >= 0) {
                return totalAmount.subtract(new BigDecimal("50"));
            }
        }
  else if(PromotionTypeEnum.SECOND_HALF_PRICE == promotionType){
   // 第二件半价逻辑
   // ......
  }

 // 若没有满足任何促销条件,返回原价
 return totalAmount;
}
  • 每次增加新策略都需要修改 calculateDiscountedPrice 方法,不仅增加了方法的复杂性,还提高了引入 bug 的风险。
2. 代码膨胀与可维护性下降

具体表现:随着促销策略的增加 calculateDiscountedPrice 方法会变得越来越长,条件分支越来越多,导致代码难以阅读和维护。 例如:随着平台策略的迭代,已经累积了多种不同的促销策略,而且条件判断复杂。

public class PromotionService {

    public BigDecimal calculateDiscountedPrice(Order order) {

        BigDecimal totalAmount = order.getTotalAmount();

        PromotionTypeEnum promotionType = order.getPromotionType();

        // 固定金额折扣:满100减20
        if (PromotionTypeEnum.FIXED_AMOUNT == promotionType) {
            if (totalAmount.compareTo(new BigDecimal("100")) >= 0) {
                return totalAmount.subtract(new BigDecimal("20"));
            }
        }

        // 百分比折扣:打8折
        else if (PromotionTypeEnum.PERCENTAGE == promotionType) {
            return totalAmount.multiply(new BigDecimal("0.8"));
        }

        // 满减折扣:满300减50,满500减100
        else if (PromotionTypeEnum.THRESHOLD == promotionType) {
            if (totalAmount.compareTo(new BigDecimal("500")) >= 0) {
                return totalAmount.subtract(new BigDecimal("100"));
            }
            else if (totalAmount.compareTo(new BigDecimal("300")) >= 0) {
                return totalAmount.subtract(new BigDecimal("50"));
            }
        }

        else if (PromotionTypeEnum.SECOND_HALF_PRICE == promotionType) {
            // 第二件半价 促销策略
        }

        else if (PromotionTypeEnum.BUNDLE_DISCOUNT == promotionType) {
            // 捆绑销售折扣 促销策略
        }

        else if (PromotionTypeEnum.FLASH_SALE == promotionType) {
            // 限时抢购折扣 促销策略
        }

        else if (PromotionTypeEnum.NEW_USER_DISCOUNT == promotionType) {
            // 新用户折扣 促销策略
        }

        else if (PromotionTypeEnum.MEMBERSHIP_DISCOUNT == promotionType) {
            // 会有折扣 促销策略
        }

        else if (PromotionTypeEnum.HOLIDAY_SPECIAL == promotionType) {
            // 节日特惠 促销策略
        }

        else if (PromotionTypeEnum.CLEARANCE_SALE == promotionType) {
            // 清仓特价 促销策略
        }

        return totalAmount;
    }

}
  • 这种判断方式,谁看都是一脸懵。
3.测试困难

具体表现:所有促销策略的逻辑都集中在 calculateDiscountedPrice 方法中,使得单元测试变得困难,每次修改一个策略,都需要重新测试所有策略,以确保没有破坏现有功能,由于所有策略都在同一个方法中,修改一个策略可能会意外影响其他策略。

4.策略复用困难

具体表现:促销策略的逻辑与 PromotionService 类紧密耦合,难以在其他地方复用,并且需要修改逻辑时,需要修改多个重复逻辑。 案例:假设现在需要增加会员积分系统中使用部分折扣计算逻辑,只能重复编写 calculateDiscountedPrice 方法的部分逻辑。

public class MemberPointsService{

 public BigDecimal calculatePointsDiscount(Member member, BigDecimal amount){
 
  PromotionTypeEnum discountType = member.getDiscountType();
  
   // 固定金额折扣:满100减20
        if (PromotionTypeEnum.FIXED_AMOUNT == promotionType) {
            if (totalAmount.compareTo(new BigDecimal("100")) >= 0) {
                return totalAmount.subtract(new BigDecimal("20"));
            }
        }

        // 百分比折扣:打8折
        else if (PromotionTypeEnum.PERCENTAGE == promotionType) {
            return totalAmount.multiply(new BigDecimal("0.8"));
        }
  
  return amount;
 }
}

策略模式

什么是策略模式

策略模式(StrategyPattern)是一种行为型设计模式,定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。

用生活中的例子来理解:去超市购物,结账时可以选择不同的支付方式,现金、信用卡、移动支付等,每种只会方式都是一种"策略",它们的目标相同,都是完成支付,但实现方式不同,超市收银台不需要关心你选择哪种支付方式,它只需要根据你的选择调用相应的支付处理流程即可。

策略模式的结构

  1. 策略接口(Strategy): 定义所有支持的算法的公共接口,Context使用这个接口来调用具体策略定义的算法。
  2. 具体策略(Concrete Strategy): 实现策略接口的具体算法。
  3. 上下文(Context): 维护一个对策略对象的引用,负责将客户端请求委派给策略对象,Context 可以定义一个接口让 Strategy 访问它的数据。

策略模式重构

重构思路

  1. 定义策略接口,新增 PromotionStrategy 接口,声明计算折扣的方法。
  2. 实现具体的策略类,新增 FixedAmountStrategyPercentageStrategyThresholdStrategy
  3. 创建上下文类,新增 PromotionContext 类,持有当前选择的促销策略,并委托策略对象进行折扣计算。

重构代码

策略接口
public interface PromotionStrategy {

    /**
     * 计算折扣后的价格
     *
     * @param amount 原始金额
     * @return 折扣后的金额
     */
    BigDecimal calculateDiscountedPrice(BigDecimal amount);

}
  • 接口 calculateDiscountedPrice(),用于计算折扣后的价格,所有的具体策略类都将实现这个接口。
具体策略类
public class FixedAmountStrategy implements PromotionStrategy {

    /**
     * 满减门槛
     */
    private BigDecimal threshold;

    /**
     * 折扣金额
     */
    private BigDecimal discountAmount;

    /**
     * 构造函数
     *
     * @param threshold      满减门槛
     * @param discountAmount 折扣金额
     */
    public FixedAmountStrategy(BigDecimal threshold, BigDecimal discountAmount) {
        this.threshold = threshold;
        this.discountAmount = discountAmount;
    }

    @Override
    public BigDecimal calculateDiscountedPrice(BigDecimal amount) {

        // 如果订单金额达到门槛,应用折扣
        if (amount.compareTo(threshold) >= 0) {
            return amount.subtract(discountAmount);
        }

        // 返回原价
        return amount;
    }
}
  • 实现了 PromotionStrategy 接口,提供了固定金额折扣的逻辑,包含两个属性:thresholddiscountAmount,当订单金额达到门槛时,应用折扣,否则返回原价。
public class PercentageStrategy implements PromotionStrategy {

    /**
     * 折扣百分比(0.8 表示 8 折)
     */
    private BigDecimal discountPercent;

    /**
     * 构造函数
     *
     * @param discountPercent
     */
    public PercentageStrategy(BigDecimal discountPercent) {
        this.discountPercent = discountPercent;
    }

    @Override
    public BigDecimal calculateDiscountedPrice(BigDecimal amount) {
        return amount.multiply(discountPercent);
    }
}
  • 该类实现了百分比折扣的逻辑,包含一个属性 discountPercent,表示折扣百分比,计算折扣时,直接将订单金额乘以折扣百分比。
public class ThresholdStrategy implements PromotionStrategy {

    private Map<BigDecimalBigDecimal> thresholdDiscountMap;

    public ThresholdStrategy() {
        this.thresholdDiscountMap = new TreeMap<>(Collections.reverseOrder());
    }

    public void addThresholdDiscount(BigDecimal threshold, BigDecimal discount) {
        thresholdDiscountMap.put(threshold, discount);
    }

    @Override
    public BigDecimal calculateDiscountedPrice(BigDecimal amount) {


        for (Map.Entry<BigDecimalBigDecimal> threshold : thresholdDiscountMap.entrySet()) {
            if (amount.compareTo(threshold.getKey()) >= 0) {
                return amount.subtract(threshold.getValue());
            }
        }

        return amount;
    }
}
  • 该类实现了满减折扣的逻辑,使用 TreeMap 并按照键的降序排列,来存储不同阈值对应的折扣金额,计算折扣时,遍历阈值-折扣折射,找到第一个小于等于订单金额的阈值,应用对应的折扣。
上下文类
public class PromotionContext {

    /**
     * 当前选择的促销策略
     */
    private PromotionStrategy strategy;

    public PromotionContext(PromotionStrategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(PromotionStrategy strategy) {
        this.strategy = strategy;
    }

    /**
     * 计算折扣后的价格
     *
     * @param amount 原始金额
     * @return 折扣后的金额
     */
    public BigDecimal executeStrategy(BigDecimal amount) {
        return strategy.calculateDiscountedPrice(amount);
    }
}
  • 该类持有当前选择的促销策略,并提供了设置策略和执行策略的方法,计算折扣时,将请求委托给当前的策略对象。
订单类
public class Order {

    private BigDecimal totalAmount;

    public Order(BigDecimal totalAmount) {
        this.totalAmount = totalAmount;
    }

    public BigDecimal getTotalAmount() {
        return totalAmount;
    }

    public void setTotalAmount(BigDecimal totalAmount) {
        this.totalAmount = totalAmount;
    }

}
测试类
public class OrderTest {

    /**
     * 测试 固定金额折扣策略
     */
    @Test
    public void test_fixedStrategy() {

        Order order = new Order(new BigDecimal("150"));

        FixedAmountStrategy fixedAmountStrategy = new FixedAmountStrategy(new BigDecimal("100"), new BigDecimal("20"));

        PromotionContext promotionContext = new PromotionContext(fixedAmountStrategy);

        BigDecimal bigDecimal = promotionContext.executeStrategy(order.getTotalAmount());
        System.out.println("订单原始价格为: " + order.getTotalAmount().toString());
        System.out.println("订单使用固定金额折扣后价格为: " + bigDecimal.toString());
    }

    /**
     * 测试 百分比折扣策略
     */
    @Test
    public void test_percentStrategy() {
        Order order = new Order(new BigDecimal("200"));
        PercentageStrategy percentageStrategy = new PercentageStrategy(new BigDecimal("0.8"));

        PromotionContext promotionContext = new PromotionContext(percentageStrategy);

        BigDecimal bigDecimal = promotionContext.executeStrategy(order.getTotalAmount());
        System.out.println("订单原始价格为: " + order.getTotalAmount().toString());
        System.out.println("订单使用百分比折扣后价格为: " + bigDecimal.toString());
    }

    /**
     * 测试 满减折扣策略
     */
    @Test
    public void test_thresholdStrategy() {
        Order order1 = new Order(new BigDecimal("350"));
        Order order2 = new Order(new BigDecimal("600"));

        ThresholdStrategy thresholdStrategy = new ThresholdStrategy();
        thresholdStrategy.addThresholdDiscount(new BigDecimal("500"), new BigDecimal("100"));
        thresholdStrategy.addThresholdDiscount(new BigDecimal("300"), new BigDecimal("50"));

        PromotionContext promotionContext = new PromotionContext(thresholdStrategy);

        BigDecimal bigDecimal1 = promotionContext.executeStrategy(order1.getTotalAmount());
        System.out.println("订单1原始价格为: " + order1.getTotalAmount().toString());
        System.out.println("订单1使用满减折扣后价格为: " + bigDecimal1.toString());

        System.out.println();

        BigDecimal bigDecimal2 = promotionContext.executeStrategy(order2.getTotalAmount());
        System.out.println("订单2原始价格为: " + order2.getTotalAmount().toString());
        System.out.println("订单2使用满减折扣后价格为: " + bigDecimal2.toString());
    }

}

重构后的优势

通过使用策略模式,将不同的促销策略封装在独立的类中,使得系统更加灵活和可扩展,主要改进包括:

  1. 分离关注点:每个策略类只关注自己的算法实现,上下文类负责与策略对象交互,客户端代码负责选择合适的策略。
  2. 消除条件判断:不再使用大量的条件判断语句来选择不同的策略,而是通过对象组合的方式动态选择策略。
  3. 提高可扩展性:添加新的促销策略只需要创建一个新的策略类,而不需要修改现有代码。
  4. 增强可测试性:每个策略类都可以独立测试,不会相互影响。
  5. 提高代码复用性:策略类可以在不同的上下文中复用。

长话短说

核心思想

策略模式 是一种行为型设计模式,核心思想可概况为以下几点:

1. 算法的封装与分离

策略模式将一系列算法封装到独立的策略类中,使他们可以相应替换,每个策略代表一种特定的算法或行为,并实现相同接口。这种封装使得算法可以独立于使用它的客户端而变化,实现算法与客户端的解耦。

2. 组合优于继承

策略模式鼓励使用对象组合而非继承来实现系统的灵活性,通过在上下文类中持有策略接口的引用,可以在运行时动态切换不同的策略,而不需要通过继承来扩展功能,这种方式更加灵活,避免了继承带来的类爆炸问题。

3. 开闭原则的体现

策略模式是开闭原则的典型应用,当需要添加新的算法或行为时,只需要创建新的策略类,而不需要修改现有代码,这使得系统更加稳定,降低了修改带来的风险。

4. 消除条件判断

策略模式通过对象组合的方式替代了复杂的条件判断语句,使得代码逻辑更加清晰、易于理解和维护,每个策略类专注于实现自己的算法,而上下文类负责选择和使用合适的策略。

适合应用场景

  1. 当你想使用对象中各种不同的算法变体,并希望能在运行时切换算法时,可以使用策略模式。策略模式让你能够将对象关联至可以不同方式执行特定子任务的不同子对象,从而以间接的方式在运行时更改对象行为。
  2. 当你有许多仅在执行某些行为时略有不同的相似类时,可使用策略模式。策略模式能让你将不同行为抽取到一个独立类层次结构中,并将原始类组合成同一个,从而减少重复代码。
  3. 如果算法在上下文的逻辑中不是特别重要,使用该模式能将类的业务逻辑与其算法实现细节隔离开。策略模式让你能将各种算法的代码、内部数据和依赖关系与其他代码隔离,不同客户端可通过一个接单的接口执行算法,并能在运行时进行切换。
  4. 当类中使用了复杂条件运算符以在同一算法的不同变体中切换时,可以使用该模式。策略模式将所有继承自同样接口的算法抽取到独立类中,因此不再需要条件语句。原始对象并不实现所有算法的变体,而是将执行工作委派给其中的一个独立算法对象。

实现方式

  1. 从上下文类中找出修改频率较高的算法(也可以是用于在运行时选择某个算法变体的通用策略接口)
  2. 声明该算法所有变体的通用策略接口。
  3. 将算法逐一抽取到各自的类中,它们都必须实现策略接口。
  4. 在上下文类中添加一个成员变量用于保存对于策略对象的引用,然后提供设置器以修改该成员变量。上下文仅可通过策略接口同策略对象进行交互,如有需要还可以定义一个接口来让策略访问其数据。
  5. 客户端必须将上下文类与相应策略进行关联,使上下文可以以预期的方式完成其主要工作。

实施步骤

1.识别变化的部分

首先,识别系统中可能变化的部分,特别是那些基于条件选择不同算法或行为的部分,这些变化的部分是策略模式的候选对象。

2.定义策略接口

定义一个策略接口,声明所有策略类共同的方法,这个接口应该足够抽象,能够适应所有可能的策略实现。

3.实现具体策略类

为每种算法或行为创建一个具体的策略类,实现策略接口。每个策略类应该专注于实现自己的算法,不应该依赖于其他策略类或者上下文类。

4.创建上下文类

创建上下文类,持有策略接口的引用,并提供方法来设置和使用策略。上下文类应该与策略接口交互,而不是与具体策略类交互。

5.在客户端代码的使用

在客户端代码中,根据需要创建具体的策略对象,并将其传递给上下文对象,客户端可以在运行时切换不同的策略,改变上下文的行为。

6.策略的创建和管理

在实际应用中,可能需要考虑策略对象的创建和管理方式,常见的方式包括:

  1. 简单工厂:使用工厂方法创建策略对象,隐藏策略的创建细节。
  2. 依赖注入:在Spring等框架中,可以使用依赖注入自动创建和管理策略对象。
  3. 策略注册表:使用Map或其他数据结构存储策略对象,根据键值查找和使用策略。

实际应用示例

1.电商促销系统

电商平台需要支持多种促销策略,如固定金额折扣、百分比折扣、减慢折扣等,使用策略模式可以灵活的切换不同的促销策略,而不需要修改订单处理结果。

2.支付系统

支付系统需要支持多种支付方式,如 信用卡支付、支付宝支付、微信支付等。使用策略模式可以将不同的支付逻辑封装在独立的策略类中,客户端可以根据用户选择的支付方式切换不同的支付策略。

3.数据验证

系统需要对不同类型的数据进行验证,如邮箱验证、手机号验证、身份证验证等。使用策略模式可以将不同的验证逻辑封装在独立的策略类中,根据数据类型选择合适的验证策略。

4.排序算法

系统需要支持多种排序算法,如冒泡排序、快速排序、归并排序等,使用策略模式可以将不同的排序算法封装在独立的策略类中,根据数据特性和性能需求选择合适的排序策略。