架构师内功心法,属于游戏设计模式的策略模式详解

一、策略模式的应用场景

策略模式(Strategy Pattern)是指定义了算法家族、分别封装起来,让它们之间可以相互替换,此模式让算法的变化不会影响到使用算法的用户。

1.1 应用场景

  • 假如系统中有很多类,而他们的区别仅仅在于他们的行为不同。
  • 一个系统需要动态地在几种算法中选择一种。

1.2 实现餐饮行业选择支付方式的业务场景

我们在外面去吃饭的时候,不同的饭店经常会有优惠活动,优惠策略也有很多很多,比如优惠券折扣、返现促销、拼团下单等等。我们来用程序模拟这样的业务场景,首先创建一个促销策略的接口:

1
2
3
4
public interface IPromotionStrategy {

void doPromotion();
}

然后分别创建优惠券抵扣策略 CouponStrategy 类:

1
2
3
4
5
6
public class CouponStrategy implements IPromotionStrategy {
@Override
public void doPromotion() {
System.out.println("领取的优惠券在指定时间到店消费,订单的价格直接减优惠券面额抵扣!");
}
}

返现促销策略 CashBackStrategy 类:

1
2
3
4
5
6
public class CashBackStrategy implements IPromotionStrategy {
@Override
public void doPromotion() {
System.out.println("返现促销,返回的金额转到支付账号!");
}
}

拼团优惠策略 GroupBuyStrategy 类:

1
2
3
4
5
6
public class GroupBuyStrategy implements IPromotionStrategy {
@Override
public void doPromotion() {
System.out.println("拼团,满5人成团,全团享受团购价!");
}
}

无优惠策略 EmptyStrategy 类:

1
2
3
4
5
6
public class EmptyStrategy implements IPromotionStrategy {
@Override
public void doPromotion() {
System.out.println("无促销活动!");
}
}

然后创建促销活动方案 PromotionActivity 类:

1
2
3
4
5
6
7
8
9
10
11
12
public class PromotionActivity {

private IPromotionStrategy promotionStrategy;

public PromotionActivity(IPromotionStrategy promotionStrategy) {
this.promotionStrategy = promotionStrategy;
}

public void execute() {
promotionStrategy.doPromotion();
}
}

编写测试类:

1
2
3
4
5
6
public static void main(String[] args) {
PromotionActivity activity_618 = new PromotionActivity(new CouponStrategy());
PromotionActivity activity_1212 = new PromotionActivity(new CashBackStrategy());
activity_618.execute();
activity_1212.execute();
}

此时,上面的这段测试代码放到实际的业务场景并不实用,因为餐饮门店做活动的时候是要根据不同需求对促销策略进行动态选择的,并不会一次性执行多种优惠。所以代码会这样写:

1
2
3
4
5
6
7
8
9
10
 public static void main(String[] args) {
PromotionActivity promotionActivity = null;
String promotionKey = "COUPON";
if(promotionKey.equals("COUPON")){
promotionActivity = new PromotionActivity(new CouponStrategy());
}else if(promotionKey.equals("CASHBACK")){
promotionActivity = new PromotionActivity(new CashBackStrategy());
}//......
promotionActivity.execute();
}

这样改造之后,满足了业务需求,客户可根据自己的需求选择不同的优惠策略了。但是,经过一段时间的业务积累,我们的促销活动会越来越多。于是,我们的程序猿小哥哥就忙不赢了,每次上活动之前都要通宵改代码,而且要做重复测试,判断逻辑可能也变得越来越复杂。这时候,我们是不需要思考代码是不是应该重构了?

其实我们可以结合工厂模式和单例模式来进行优化改造。创建PromotionActivityFactory:

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
public class PromotionActivityFactory {

public interface PromotionKey {
String COUPON = "COUPON";
String CASHBACK = "CASHBACK";
String GROUPBUY = "GROUPBUY";
}

private static Map<String, IPromotionStrategy> PROMOTION_STRATEGY_MAP =
new HashMap<>();
static {
PROMOTION_STRATEGY_MAP.put(PromotionKey.COUPON, new CouponStrategy());
PROMOTION_STRATEGY_MAP.put(PromotionKey.CASHBACK, new CashBackStrategy());
PROMOTION_STRATEGY_MAP.put(PromotionKey.GROUPBUY, new GroupBuyStrategy());
}

private static final IPromotionStrategy NO_PROMOTION = new EmptyStrategy();

private PromotionActivityFactory() {}

public static IPromotionStrategy getPromotionStrategy(String promotionKey) {
IPromotionStrategy promotionStrategy = PROMOTION_STRATEGY_MAP.get(promotionKey);
return promotionStrategy == null ? NO_PROMOTION : promotionStrategy;
}

}

测试代码如下:

1
2
3
4
5
6
public static void main(String[] args) {
String promotionKey = "COUPON";
PromotionActivity promotionActivity = new PromotionActivity(PromotionActivityFactory.getPromotionStrategy(promotionKey));
promotionActivity.execute();

}

修改代码之后维护工作应该轻松了很多,每次上新活动,不会影响原来的代码逻辑。来看一下完整的类图:

为了加深大家对策略模式的理解,结合实际生活场景再举一个例子。大家都用过移动支付进行付款,比较流行的支付方式有支付宝、微信、银联等。一个场景的支付场景就是在支付的时候提示会选择支付方式,如果用户没有进行选择,那么系统会使用默认的支付方式进行结算。

创建抽象类Payment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class Payment {

public abstract String getType();

public PayState pay(String id, double amount) {
if(queryBalance(id) < amount) {
return new PayState(500,"支付失败","余额不足");
}
return new PayState(200,"支付成功","支付金额:" + amount);
}

public abstract double queryBalance(String id);

}

分别创建具体的支付方式,支付宝 AliPay 类:

1
2
3
4
5
6
7
8
9
10
11
public class AliPay extends Payment {
@Override
public String getType() {
return "支付宝";
}

@Override
public double queryBalance(String id) {
return 1000;
}
}

微信支付 WechatPay 类:

1
2
3
4
5
6
7
8
9
10
11
public class WechatPay extends Payment {
@Override
public String getType() {
return "微信支付";
}

@Override
public double queryBalance(String id) {
return 512;
}
}

银联云闪付支付 UnionPay 类:

1
2
3
4
5
6
7
8
9
10
11
public class UnionPay extends Payment {
@Override
public String getType() {
return "云闪付";
}

@Override
public double queryBalance(String id) {
return 380;
}
}

创建支付状态的包装类 PayState:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

public class PayState {

private int code;
private Object data;
private String msg;

public PayState(int code, Object data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
}

@Override
public String toString() {
return "PayState{" +
"code=" + code +
", data=" + data +
", msg='" + msg + '\'' +
'}';
}
}

创建支付策略管理工厂类:

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
public class PayStrategyFactory {

public interface PayKey {
String DEFAULTPAY = "ALIPAY";
String ALIPAY = "ALIPAY";
String WECHATPAY = "WECHATPAY";
String UNIONPAY = "UNIONPAY";
}

public static final Map<String, Payment> PAYMENT_MAP = new HashMap<>();

static {
PAYMENT_MAP.put(PayKey.ALIPAY, new AliPay());
PAYMENT_MAP.put(PayKey.WECHATPAY, new WechatPay());
PAYMENT_MAP.put(PayKey.UNIONPAY, new UnionPay());
}

private static Payment getPayment(String payKey) {
if(!PAYMENT_MAP.containsKey(payKey)){
return PAYMENT_MAP.get(PayKey.DEFAULTPAY);
}
return PAYMENT_MAP.get(payKey);
}

}

创建订单 Order 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Order {

private String id;
private String orderId;
private double amount;

public Order(String id, String orderId, double amount) {
this.id = id;
this.orderId = orderId;
this.amount = amount;
}

public PayState pay(){
return pay(PayStrategyFactory.PayKey.DEFAULTPAY);
}

public PayState pay(String payKey){
Payment payment = PayStrategyFactory.PAYMENT_MAP.get(payKey);
System.out.println("欢迎使用" + payment.getType());
System.out.println("本次交易金额为:" + amount + ",开始扣款...");
return payment.pay(id,amount);
}
}

测试代码如下:

1
2
3
4
5
6
public static void main(String[] args) {

Order order = new Order("1", "20200225000001", 120.98);

System.out.println(order.pay(PayStrategyFactory.PayKey.ALIPAY));
}

运行结果:

最后来看一下类图结构:

二、源码中的策略模式

2.1 Compartor接口

Compartor接口中的compare()方法就是一个策略模式的抽象实现。

int compare(T o1, T o2);

Comparator 接口下面有非常多的实现类,我们经常会把 Comparator 作为参数传入作为排序策略,例如 Arrays 类的 parallelSort 方法等:

还有 TreeMap 的构造方法:

2.2 Spring中的策略模式

2.2.1 Resouce类

我们来看Resource类的源码:

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
42
43
44
45
46
package org.springframework.core.io;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import org.springframework.lang.Nullable;

public interface Resource extends InputStreamSource {
boolean exists();

default boolean isReadable() {
return this.exists();
}

default boolean isOpen() {
return false;
}

default boolean isFile() {
return false;
}

URL getURL() throws IOException;

URI getURI() throws IOException;

File getFile() throws IOException;

default ReadableByteChannel readableChannel() throws IOException {
return Channels.newChannel(this.getInputStream());
}

long contentLength() throws IOException;

long lastModified() throws IOException;

Resource createRelative(String var1) throws IOException;

@Nullable
String getFilename();

String getDescription();
}

我们虽然没有直接使用 Resource 类,但是我们经常使用它的子类,例如:

Spring 的初始化也采用了策略模式,不同的类型的类采用不
同的初始化策略。首先有一个 InstantiationStrategy 接口,我们来看一下源码:

1
2
3
4
5
6
7
public interface InstantiationStrategy {
Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3) throws BeansException;

Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3, Constructor<?> var4, Object... var5) throws BeansException;

Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3, @Nullable Object var4, Method var5, Object... var6) throws BeansException;
}

顶层的策略抽象非常简单,但是它下面有两种策略 SimpleInstantiationStrategyCglibSubclassingInstantiationStrategy,我们看一下类图:


打开类图我们还发现 CglibSubclassingInstantiationStrategy 策略类还继承了
SimpleInstantiationStrategy类,说明在实际应用中多种策略之间还可以继承使用。

三、策略模式的优缺点

优点:

  • 策略模式符合开闭原则;
  • 避免使用多重条件转移语句,如 if…else…语句、switch 语句;
  • 使用策略模式可以提高算法的保密性和安全性。

缺点:

  • 客户端必须知道所有的策略,并且自行决定使用哪一个策略类;
  • 代码中会产生非常多策略类,增加维护难度。