設計模式【12】-- 搞定最近大火的策略模式

第十六封發表於2022-01-23

開局還是那種圖,最近策略模式貌似很火,各位客官往下看...

設計模式

策略模式到底是什麼?

前面我們其實已經將結構型模式講解完了,剩下的全都是行為型模式,三種模式的區分:

  • 建立型模式:如何建立一個物件
  • 結構型模式:物件內部的構造是如何構造的
  • 行為型模式:物件是如何執行(可以做什麼)

而提到策略模式,我們該如何理解呢?

  • 從北京到上海,可以坐飛機,也可以坐動車,也可以坐綠皮,甚至可以騎自行車,這些不同的方式,就是策略。

  • 一件商品,可以直接打 5 折,也可以加 100 再打 6 折,也可以打 5.5 折之後返回現金券,這些也是策略。

  • 帶了 200 去超市買東西,可以買零食,也可以買生活用品,也可以啥也不買,這些也是策略。

上面的例子,其實我們可以發現不同的策略其實是可以互換的,甚至策略細節之間可以混搭,那麼很多時候我們為了方便擴充,方便替換策略,對呼叫方遮蔽不同的策略細節,就可以使用策略模式。倘若所有的策略全部寫在一個類裡面,可以是可以,這樣維護程式碼會越來越複雜,維護只會越來越困難,裡面會充斥著各種if-else,演算法如果日益複雜,動一發而牽全身,那就只剩下提桶跑路了。

策略模式是指有一定行動內容的相對穩定的策略名稱。策略模式在古代中又稱“計策”,簡稱“計”,如《漢書·高帝紀上》:“漢王從其計”。這裡的“計”指的就是計謀、策略。策略模式具有相對穩定的形式,如“避實就虛”、“出奇制勝”等。一定的策略模式,既可應用於戰略決策,也可應用於戰術決策;既可實施於大系統的全域性性行動,也可實施於大系統的區域性性行動。

再說一個打工人都知道的例子,比如每個人都要交個人所得稅,我們知道個人所得稅的計算方式是很複雜的,稅法計算就是一個行為,而可能存在不同實現演算法,那每一種計算演算法都是一種策略。

這些策略可能隨時被替換掉,那麼我們為了好替換,相互隔離,就定義一系列演算法,封裝起來,這就叫策略模式。

策略模式的角色

策略模式一般有三種角色:

  • 抽象的策略類(Strategy):將策略的行為抽象出公共介面
  • 具體的策略類(ConcreteStrategy):以Strategy介面實現某些具體的演算法,具體的策略可能多個,也可以相互替換
  • 上下文(Context): 一般是封裝了策略,以及策略的使用方法,對上層呼叫遮蔽了細節,直接呼叫即可。

策略模式的demo

建立一個抽象的策略介面:

public interface Strategy {

    int doStrategy();
}

建立 2 個具體的策略類,分別執行策略:

public class ConcreteStrategy1 implements Strategy{
    @Override
    public int doStrategy() {
        System.out.println("執行策略1...");
        return 1;
    }
}

public class ConcreteStrategy2 implements Strategy{
    @Override
    public int doStrategy() {
        System.out.println("執行策略2...");
        return 2;
    }
}

建立上下文類,封裝策略類:

public class Context {
    private Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public void executeStrategy(){
        strategy.doStrategy();
    }
}

測試類:

public class Test {
    public static void main(String[] args) {
        Context context1 = new Context(new ConcreteStrategy1());
        context1.executeStrategy();

        context1 = new Context(new ConcreteStrategy2());
        context1.executeStrategy();
    }
}

測試結果:

執行策略1
執行策略2

可以看到只要使用不同的策略,就可以執行不同的結果。

分紅包策略實戰

比如,工作中有涉及到分配的策略,場景就是有紅包,分配給若干個人,但是會有不同分配策略。這裡不同的分配策略其實就是用策略模式來實現。可以隨機分配,也可以平均分配,還可以根據各種權重來分配等等。這裡只演示兩種分配:

首先定義一個紅包類,包含紅包金額(我們以分為單位,儲存整數,避免金額小數不準確問題

public class RedPackage {
    public int remainSize;

    public int remainMoney;

    public RedPackage(int remainSize, int remainMoney) {
        this.remainSize = remainSize;
        this.remainMoney = remainMoney;
    }
}

定義一個分配策略的抽象類:

import java.util.List;

public interface AllocateStrategy {

    List<Integer> allocate(RedPackage redPackage);

}

平均分配的時候,如果不能平均,那麼第一位會有所增減:

import java.util.ArrayList;
import java.util.List;

public class AverageAllocateStrategy implements AllocateStrategy {
    @Override
    public List<Integer> allocate(RedPackage redPackage) {
        List<Integer> results = new ArrayList<>();
        Integer sum = redPackage.remainMoney;
        Integer average = sum / redPackage.remainSize;
        for (int i = 0; i < redPackage.remainSize; i++) {
            sum = sum - average;
            results.add(average);
        }
        if (sum != 0) {
            results.set(0, results.get(0) + sum);
        }
        return results;
    }
}

隨機分配的時候,我們將每一份隨機新增到紅包裡面去,但是這樣不能保證每一份都有金額(注意這個不要在生產使用,僅限測試


import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class RandomAllocateStrategy implements AllocateStrategy {
    @Override
    public List<Integer> allocate(RedPackage redPackage) {
        return ranRedPackage(redPackage.remainSize, redPackage.remainMoney);
    }

    public List<Integer> ranRedPackage(Integer count, Integer money) {
        List<Integer> result = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            result.add(0);
        }
        for (int i = 1; i <= money; i++) {
            int n = new Random().nextInt(count);
            result.set(n, result.get(n) + 1);
        }
        return result;
    }

}

定義上下文類,封裝不同的策略:

import java.util.List;

public class Context {
    private AllocateStrategy strategy;

    public Context(AllocateStrategy strategy) {
        this.strategy = strategy;
    }

    public List<Integer> executeStrategy(RedPackage redPackage){
        return strategy.allocate(redPackage);
    }
}

測試類:

import java.util.List;

public class Test {
    public static void main(String[] args) {
        RedPackage redPackage = new RedPackage(10,102);
        Context context = new Context(new RandomAllocateStrategy());
        List<Integer> list =context.executeStrategy(redPackage);
        list.forEach(l-> System.out.print(l+" "));

        System.out.println("");
        context = new Context(new AverageAllocateStrategy());
        list =context.executeStrategy(redPackage);
        list.forEach(l-> System.out.print(l+" "));
    }
}

可以看到分配的金額確實會隨著策略類的不同發生不同的變化:

9 10 16 8 14 8 7 15 9 6 
12 10 10 10 10 10 10 10 10 10 

注意這個不能在生產使用!!!

優缺點

策略模式的優點:

  • 消除if-else語句,不同策略相互隔離,方便維護
  • 切換自由
  • 擴充性好,實現介面即可

策略模式的缺點:

  • 策略一旦數量過多,維護也會相對比較難,複用程式碼比較難
  • 所有的策略類都對外暴露了,雖然一般通過Context上下文呼叫

策略模式比較常用,可能有些同學會混淆策略模式和狀態模式:

相對於狀態模式:策略模式只會執行一次方法,而狀態模式會隨著狀態變化不停的執行狀態變更方法。舉個例子,我們從A地方去B地方,策略就是飛機或者火車,狀態模式則是可能到某一個地方,換了一種交通方式,到另外一個地方又換了一種交通方式。

小結

策略模式比較常用,核心就是隔離不同的策略,將具體的演算法封裝在策略裡面,抽象出一個策略抽象類,把不同具體策略類注入到上下文類中,達到選擇不同策略的目的。

但是如果策略很多,需要考慮複用,策略類對外暴露的時候需要考慮濫用風險,會破壞封裝性。

【作者簡介】
秦懷,公眾號【秦懷雜貨店】作者,個人網站:http://aphysia.cn,技術之路不在一時,山高水長,縱使緩慢,馳而不息。

設計模式系列:

相關文章