策略模式(Strategy)

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

策略模式

简要介绍

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

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

使用场景

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

类图

例子

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

正常情况

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

就是写出了以下的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 参数,表示满减金额

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

使用策略模式

我们先定义一个顶层接口

1
2
3
4
5
6
7
8
9
10
11
12
public interface ICouponDiscount<T> {


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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}
}

集成所有算法的上下文类

1
2
3
4
5
6
7
8
9
10
11
12
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);
}
}

客户端使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 里面写就行,而不用客户端每次调用接口都要记得写

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 类中的主方法:

1
2
3
4
5
6
7
8
优惠金额计算中... 原价:10.5
满减策略,满 10.5101
优惠金额计算中... 原价:10.5
直减策略,10.5101
优惠金额计算中... 原价:10.5
折扣策略,10.5 打一折:1.05
优惠金额计算中... 原价:10.5
n元购策略:直接n元买下:0.5

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