Java 設計模式實戰系列—策略模式

2023-06-25 12:01:41

從優惠打折活動說起

電商平臺為了增加銷量經常搞一些活動,比如 618、雙十一,還有一些節假日活動,根據銷量的變化又經常更新不同的活動。最開始為了增加銷量,全場都六折:

// 打六折
public BigDecimal sixDiscount(BigDecimal amount) {
    BigDecimal discount = BigDecimal.valueOf(0.6);
    return amount.multiply(discount);
}

促銷了幾天之後,發現銷量上去了,但是利潤又減少了,又改成打八折了:

// 打八折
public BigDecimal eightDiscount(BigDecimal amount) {
    BigDecimal discount = BigDecimal.valueOf(0.8);
    return amount.multiply(discount);
}

再過幾個月,活動每週都可能會改變,類似這週六折,下週八折,每次修改活動,就要改程式碼,就比較繁瑣。後面就根據引數來選擇不同的活動:

// 根據不同的type,選擇不同的打折方案
public BigDecimal simpleDiscount(int type,BigDecimal amount) {
    if (type == 1) {
        amount = amount.multiply(BigDecimal.valueOf(0.6));
    } else if (type == 2) {
        amount = amount.multiply(BigDecimal.valueOf(0.8));
    }
    return amount;
}

後面不增加活動方案的前提下,改變方案只需要前端選擇對應的方案即可。但如果每次新增程式碼,還是需要更新程式碼,還需要更多繁瑣的 if-else 判斷:

// 根據不同的type,選擇不同的打折方案
public BigDecimal discount(int type,BigDecimal amount) {
    if (type == 1) {
        amount = amount.multiply(BigDecimal.valueOf(0.6));
    } else if (type == 2) {
        amount = amount.multiply(BigDecimal.valueOf(0.8));
    } else if (type == 3) {
        // 滿100減20,滿200減50
    } else if (type == 4) {
        // 滿500減70
    } else if (type == 5) {
        // 滿1000減兩百
    }
    // 更多的方案 .....
    return amount;
}

上面的方法只是一些簡單的打折方案,實際上的打折方案更復雜,一個打折方案程式碼至少要十多行,而上面十多個方案,程式碼顯得非常的臃腫。有如下幾個缺點:

  • 每次增加方案,只能在方面裡面新增,導致方案的程式碼越來越多,一個方法有幾百行程式碼。耦合度非常高。
  • 臃腫的程式碼每次都要從頭看到尾,可讀性比較差。
  • 使用繁瑣的 if-else 或者 switch 分支判斷,可讀性比較差。

解決上面的幾個問題一個比較好的方案就是使用策略模式,策略模式可以避免繁瑣的 if-else 分支判斷,同時降低程式碼的耦合度,增強程式碼的可讀性。

策略模式的定義和實現

定義一系列的演演算法,把每個演演算法封裝起來, 並且使它們可相互替換。根據呼叫者傳參來呼叫具體的演演算法。將策略的定義、建立和使用三個部分解耦。下面就上面的打折方案的方法使用策略模式進行改造。

策略的定義

策略的定義包含一個策略介面和一組實現這個介面的策略類,所有的策略類都實現相同的介面。

介面和實現類如下:

// 建立一個介面
public interface Strategy {
    BigDecimal discount(BigDecimal amount);
}

// 打六折
public class SixDiscountStrategy implements Strategy{
    @Override
    public BigDecimal discount(BigDecimal amount) {
        return amount = amount.multiply(BigDecimal.valueOf(0.6));
    }
}

// 打八折
public class EightDiscountStrategy  implements Strategy{
    @Override
    public BigDecimal discount(BigDecimal amount) {
        return amount = amount.multiply(BigDecimal.valueOf(0.8));
    }
}

// 滿100減20,滿200減50
public class FirstDiscountStrategy implements Strategy{
    @Override
    public BigDecimal discount(BigDecimal amount) {
        // 滿100減20,滿200減50
        // 省略具體演演算法....
        return amount;
    }
}

將上面一個幾百行的方法,拆分成一個一個小的類。類的數量變多了,但是程式碼也更加簡潔了。

策略建立和使用

策略模式包含一組策略,一般通過型別 type 來選擇建立哪個策略類。將建立策略類的程式碼封裝成一個工具類,通過 type 直接呼叫:

// 策略工具類
public class Context {
  public static Strategy operation(int type) {
      if (type == 1) {
          return new SixDiscountStrategy();
      } else if (type == 2) {
          return new EightDiscountStrategy();
      } else if (type == 3) {
          return new FirstDiscountStrategy();
      }
      throw new IllegalArgumentException("not fond strategy");
  }
}

以上根據不同的 type 呼叫不同的策略類,在執行對應的方法。呼叫策略方法就簡單多了,為了方便測試,直接使用 mian 方法呼叫:

public static void main(String[] args) {
    int type = 1;
    Strategy strategy = Context.operation(type);
    BigDecimal sixDiscount = strategy.discount(BigDecimal.valueOf(100));
    System.out.println("六折優惠價格:" + sixDiscount);
}

可能細心的同學發現,還是有很多 if-else 的程式碼,每次新增一個活動,還需要在 Context 多寫一個判斷。如果策略類是無狀態的,可以被共用,那就不需要每次呼叫都建立一個新的策略物件,實現將建立好的物件快取到 map 集合中,呼叫的時候直接,同時也不需要寫 if-else 判斷條件,優化程式碼如下:

private static Map<Integer,Strategy> map = new HashMap<>();

static {
    map.put(1,new SixDiscountStrategy());
    map.put(2,new EightDiscountStrategy());
    map.put(3,new FirstDiscountStrategy());
}

public static Strategy operation(int type) {
    Strategy strategy = map.get(type);
    if (strategy == null) {
        throw new IllegalArgumentException("not fond strategy");
    }
    return strategy;
}

public static void main(String[] args) {
    // 六折優惠
    int type = 1;
    Strategy strategy = Context.operation(type);
    BigDecimal sixDiscount = strategy.discount(BigDecimal.valueOf(100));
    System.out.println("六折優惠價格:" + sixDiscount);
}

重構之後的程式碼就沒有 if-else 分支語句了,主要是利用了策略模式和 Map 集合,通過 type 直接獲取 Map 集合上對應的策略類,從而很好的規避了 if-else 判斷。通過查表法代替 if-else 判斷。

將程式碼重構之後,程式碼也不會顯得臃腫和複雜,有如下幾個好處:

  • 每個策略都有自己的對應的類,檢視策略只需要檢視自己對應的類即可,程式碼的可讀性也大大增加。
  • 程式碼拆分到不同的策略類上,更加的簡潔。
  • 後續新增策略類,只需要新增對應的策略介面實現以及 Map 集合,程式碼整體改動量比較小。

總結

本文先從電商專案的一個複雜多變的優惠券活動上講起,優惠券的活動複雜多變,經常要搞不同的活動,可能每天也是不同的活動。針對複雜多變的需求,程式碼數量比較龐大和複雜,程式碼可讀性差,耦合度高。所以就需要使用一個設計模式簡化程式碼,減少程式碼改動量,增加程式碼的可讀性。策略模式就應允而生。

  • 策略模式包含一個策略介面和一組實現這個介面的策略類,所有的策略類都實現這個介面。策略類可以替換。
  • 策略模式用來解耦策略的定義、建立以及使用,完整的策略模式有這三個部分組成:
    • 策略類的定義包含一個介面和一組實現類,後續增加策略只需要新增對應的實現類即可。
    • 策略的建立由工具類,或者工廠方法來完成。
    • 策略的使用通過傳入引數來執行使用哪個策略,編譯就確定好的狀態只需要將不同的策略儲存在 Map 集合中查表呼叫。如果需要執行時動態確定就需要返回一個範例物件,執行時動態是比較典型的應用場景。
  • 使用策略模式之後,後面程式碼需要新增新的分支,改動量比較小,程式碼也比較清晰。
  • 策略模式最主要的作用是解耦策略的定義、建立和使用,控制程式碼的複雜度,讓程式碼量不多過多,程式碼邏輯不會過於複雜。對於複雜的程式碼來講,策略模式還能再新增新的策略時,能最小化改動程式碼。
  • 如果程式碼量比較少,邏輯不太複雜的程式碼,就不太需要引入策略模式,不然增加系統的複雜性。