策略模式(Strategy)

诸葛亮的锦囊妙计,每一个锦囊就是一个策略

策略模式

简要介绍

一个一个的策略,就是封装的一个又一个的算法,并且这些算法之间可以互换

客户端使用时,可以有选择的使用某个算法,算法和客户端解耦,我们可以用多个算法解决同一个问题

使用场景

  • 一个系统需要动态的在几种算法中选择一种(说的就是 if else 里面逻辑一大坨的情况)

类图

例子

场景描述:商品优惠有多种算法,满减、直减、折扣和 N 元购等等

正常情况

我们判断商品价格,然后判断是那种优惠策略,然后再根据优惠算法继续具体计算优惠后的钱

就是写出了以下的程序:

public class OriginalDemo {
    /**
     * 优惠券折扣计算
     * @param type 优惠券类型
     * @param typeContent 优惠券金额
     * @param skuPrice 商品金额
     * @param typeExt 满减金额
     * @return 折扣后的价格
     */
    public double discountAmount(int type, double typeContent, double skuPrice, double typeExt) {
        // 直减券
        if (type == 1) {
            return skuPrice - typeContent;
        }
        // 满减券
        if (type == 2) {
            // 不够满减金额,还按原价处理
            if (skuPrice < typeExt) {
                return skuPrice;
            }
            return skuPrice - typeContent;
        }
        // 折扣券
        if (type == 3) {
            return skuPrice * typeContent;
        }
        // n元购
        if (type == 4) {
            return typeContent;
        }
        // 类型都没有匹配上,就按原价处理
        return skuPrice;
    }

}

我们上述写法虽然也能完成功能,但是还有缺点,「扩展性不能满足」比如满减,前三个参数并不能满足这个算法,所以只能添加一个 typeExt 参数,表示满减金额

如果再出现五花八门的算法呢,这个方法还不知道加多少参数,而且违背了我们的开闭原则,重复改这个方法,极易出错

使用策略模式

我们先定义一个顶层接口

public interface ICouponDiscount<T> {


    /**
     * 优惠券金额计算
     * @param couponInfo 券折扣信息:直减、满减、折扣、n元购
     * @param skuPrice 商品金额
     * @param <T> 券折扣信息类型
     * @return 优惠后的金额
     */
     BigDecimal discountAmount(T couponInfo, BigDecimal skuPrice);
}

这样后续有啥算法直接实现这个接口,实现其中的方法就行

下面是四种优惠算法,四个类:

public class MJCouponDiscount implements ICouponDiscount<Map<String, String>> {

    /**
     * 满减计算,判断满 x 元后减去 n 元
     * 最低得支付 1 元
     * @param couponInfo 券折扣信息:直减、满减、折扣、n元购
     * @param skuPrice 商品金额
     * @return 满减后的价格
     */
    @Override
    public BigDecimal discountAmount(Map<String, String> couponInfo, BigDecimal skuPrice) {
        String x = couponInfo.get("x");
        String n = couponInfo.get("n");

        // 不够满减金额,直接返回商品原价
        if (skuPrice.compareTo(new BigDecimal(x)) < 0) {
            return skuPrice;
        }
        // 可以满减了
        BigDecimal result = skuPrice.subtract(new BigDecimal(n));
        // 最低支付 1 块钱
        if (result.compareTo(BigDecimal.ONE) < 0) {
            return BigDecimal.ONE;
        }
        return result;
    }
}
public class NYGCouponDiscount implements ICouponDiscount<Double> {

    /**
     * n 元购 直接返回n元 固定价格购买
     * @param couponInfo 券折扣信息:直减、满减、折扣、n元购
     * @param skuPrice 商品金额
     * @return 直接返回n元
     */
    @Override
    public BigDecimal discountAmount(Double couponInfo, BigDecimal skuPrice) {
        return new BigDecimal(couponInfo);
    }
}
public class ZJCouponDiscount implements ICouponDiscount<Double> {

    /**
     * 直减
     * 直接减去优惠券的数值
     * 最低付一元
     * @param couponInfo 券折扣信息:直减、满减、折扣、n元购
     * @param skuPrice 商品金额
     * @return 优惠后的价格
     */
    @Override
    public BigDecimal discountAmount(Double couponInfo, BigDecimal skuPrice) {
        BigDecimal result = skuPrice.subtract(new BigDecimal(couponInfo));
        // 可能是
        // 返回0 等于 1元
        // 返回-1 小于 1元
        // 返回1 大于 1元
        // 所以小于 1 就是(0 or -1) 小于等于 1 元
        if (result.compareTo(BigDecimal.ONE) < 1) {
            return BigDecimal.ONE;
        }
        return result;
    }
}
public class ZKCouponDiscount implements ICouponDiscount<Double> {
    /**
     * 折扣
     * 商品价格乘以折扣比例 就是最后支付金额
     * 保留两位小数
     * 最低支付1元
     * @param couponInfo 券折扣信息:直减、满减、折扣、n元购
     * @param skuPrice 商品金额
     * @return 折扣后的金额
     */
    @Override
    public BigDecimal discountAmount(Double couponInfo, BigDecimal skuPrice) {
        BigDecimal result = skuPrice.multiply(new BigDecimal(couponInfo)).setScale(2, BigDecimal.ROUND_HALF_UP);
        if (result.compareTo(BigDecimal.ONE) < 1) {
            return BigDecimal.ONE;
        }
        return result;
    }
}

集成所有算法的上下文类

public class Context<T> {

    private ICouponDiscount<T> couponDiscount;

    public Context(ICouponDiscount<T> couponDiscount) {
        this.couponDiscount = couponDiscount;
    }

    public BigDecimal discountAmount(T couponInfo, BigDecimal skuPrice) {
        return couponDiscount.discountAmount(couponInfo, skuPrice);
    }
}

客户端使用:

public class MainTest {

    public void zjTest() {
        // 直减
        Context<Double> context = new Context<>(new ZJCouponDiscount());
        BigDecimal result = context.discountAmount(10D, new BigDecimal("10.5"));
        System.out.println("直减策略,10.5 减 10:" + result);
    }

    public void mjTest() {
        // 满减
        Context<Map<String, String>> context = new Context<>(new MJCouponDiscount());
        HashMap<String, String> couponInfo = new HashMap<>();
        // 满 10.5 减 10 满 x 减 n
        couponInfo.put("x", "10.5");
        couponInfo.put("n", "10");
        BigDecimal result = context.discountAmount(couponInfo, new BigDecimal("10.5"));
        System.out.println("满减策略,满 10.5 减 10:" + result);
    }

    public void zkTest() {
        // 折扣
        Context<Double> context = new Context<>(new ZKCouponDiscount());
        BigDecimal result = context.discountAmount(0.1D, new BigDecimal("10.5"));
        System.out.println("折扣策略,10.5 打一折:" + result);
    }

    public void nyTest() {
        Context<Double> context = new Context<>(new NYGCouponDiscount());
        BigDecimal result = context.discountAmount(0.5D, new BigDecimal("10.5"));
        System.out.println("n元购策略:直接n元买下:" + result);
    }

    public static void main(String[] args) {
        MainTest mainTest = new MainTest();
        mainTest.mjTest();
        mainTest.zjTest();
        mainTest.zkTest();
        mainTest.nyTest();
    }
}

时序图

以例子中的 zjTest 方法为例,分析策略模式调用的时序图

为什么需要一个 context?我直接调接口不行吗?

可以参考上面的时序图,context 把客户端和算法接口解耦,让上下文去和算法接口打交道,而客户端只知道自己调了一个 context 的某个方法,就可以了,实际情况我们和算法接口打交道前后会有一些操作,这个时候直接 context 里面写就行,而不用客户端每次调用接口都要记得写

例如:

public class Context<T> {

    private ICouponDiscount<T> couponDiscount;

    public Context(ICouponDiscount<T> couponDiscount) {
        this.couponDiscount = couponDiscount;
    }

    public BigDecimal discountAmount(T couponInfo, BigDecimal skuPrice) {
        System.out.println("优惠金额计算中... 原价:" + skuPrice);
        return couponDiscount.discountAmount(couponInfo, skuPrice);
    }
}

我们每次计算优惠金额的时候都要知道原价多少,再次执行 MainTest 类中的主方法:

优惠金额计算中... 原价:10.5
满减策略,满 10.5101
优惠金额计算中... 原价:10.5
直减策略,10.5101
优惠金额计算中... 原价:10.5
折扣策略,10.5 打一折:1.05
优惠金额计算中... 原价:10.5
n元购策略:直接n元买下:0.5

客户端代码无需改动,直接动用上下文和算法交互即可


策略模式(Strategy)
https://www.powercheng.fun/articles/e54b0c5d/
作者
powercheng
发布于
2022年3月29日
许可协议