架構師內功心法,屬於遊戲設計模式的策略模式詳解

來一杯咖啡 發佈 2020-02-25T05:46:02+00:00

缺點:客戶端必須知道所有的策略,並且自行決定使用哪一個策略類;代碼中會產生非常多策略類,增加維護難度。

一、策略模式的應用場景

策略模式(Strategy Pattern)是指定義了算法家族、分別封裝起來,讓它們之間可以相互替換,此模式讓算法的變化不會影響到使用算法的用戶。

1.1 應用場景

  • 假如系統中有很多類,而他們的區別僅僅在於他們的行為不同。
  • 一個系統需要動態地在幾種算法中選擇一種。

1.2 實現餐飲行業選擇支付方式的業務場景

我們在外面去吃飯的時候,不同的飯店經常會有優惠活動,優惠策略也有很多很多,比如優惠券折扣、返現促銷、拼團下單等等。我們來用程序模擬這樣的業務場景,首先創建一個促銷策略的接口:

public interface IPromotionStrategy {

    void doPromotion();
}

然後分別創建優惠券抵扣策略 CouponStrategy 類:

public class CouponStrategy implements IPromotionStrategy {
    @Override
    public void doPromotion() {
        System.out.println("領取的優惠券在指定時間到店消費,訂單的價格直接減優惠券面額抵扣!");
    }
}

返現促銷策略 CashBackStrategy 類:

public class CashBackStrategy implements IPromotionStrategy {
    @Override
    public void doPromotion() {
        System.out.println("返現促銷,返回的金額轉到支付帳號!");
    }
}

拼團優惠策略 GroupBuyStrategy 類:

public class GroupBuyStrategy implements IPromotionStrategy {
    @Override
    public void doPromotion() {
        System.out.println("拼團,滿5人成團,全團享受團購價!");
    }
}

無優惠策略 EmptyStrategy 類:

public class EmptyStrategy implements IPromotionStrategy {
    @Override
    public void doPromotion() {
        System.out.println("無促銷活動!");
    }
}

然後創建促銷活動方案 PromotionActivity 類:

public class PromotionActivity {

    private IPromotionStrategy promotionStrategy;

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

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

編寫測試類:

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();
}

此時,上面的這段測試代碼放到實際的業務場景並不實用,因為餐飲門店做活動的時候是要根據不同需求對促銷策略進行動態選擇的,並不會一次性執行多種優惠。所以代碼會這樣寫:

 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:

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;
    }

}

測試代碼如下:

public static void main(String[] args) {
    String promotionKey = "COUPON";
    PromotionActivity promotionActivity =  new PromotionActivity(PromotionActivityFactory.getPromotionStrategy(promotionKey));
    promotionActivity.execute();

}

修改代碼之後維護工作應該輕鬆了很多,每次上新活動,不會影響原來的代碼邏輯。來看一下完整的類圖:

為了加深大家對策略模式的理解,結合實際生活場景再舉一個例子。大家都用過移動支付進行付款,比較流行的支付方式有支付寶、微信、銀聯等。一個場景的支付場景就是在支付的時候提示會選擇支付方式,如果用戶沒有進行選擇,那麼系統會使用默認的支付方式進行結算。

創建抽象類Payment:

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 類:

public class AliPay extends Payment {
    @Override
    public String getType() {
        return "支付寶";
    }

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

微信支付 WechatPay 類:

public class WechatPay extends Payment {
    @Override
    public String getType() {
        return "微信支付";
    }

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

銀聯雲閃付支付 UnionPay 類:

public class UnionPay extends Payment {
    @Override
    public String getType() {
        return "雲閃付";
    }

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

創建支付狀態的包裝類 PayState:


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 + '\'' +
                '}';
    }
}

創建支付策略管理工廠類:

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 類:

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);
    }
}

測試代碼如下:

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類的源碼:

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 接口,我們來看一下源碼:

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 語句;
  • 使用策略模式可以提高算法的保密性和安全性。

缺點:

  • 客戶端必須知道所有的策略,並且自行決定使用哪一個策略類;
  • 代碼中會產生非常多策略類,增加維護難度。
關鍵字: