週末無事,窩在家裡面看《權力的遊戲第八季》,看的很是津津有味,雖然感覺有一點點要爛尾,但是我還是忍不住要去看到底誰可以坐上鐵王座。
女朋友在一旁點外賣,好像是在使用優惠的時候遇到了一點點小問題。
策略,指的是可以實現目標的方案集合,在某些特定情況下,策略之間是可以相互替換的。
比如我們在外賣平臺上看到的這些優惠。滿減、會員和紅包等,每一個大項優惠都具體包含了多個優惠方案。如滿減活動中,可以同時有滿20減15、滿50減30等。會員包含普通會員、超級會員等。
每一個優惠方式下面的多個優惠方案,其實都是一個策略。這些策略之間是相互排斥、可替換的。並且是有一定的優先順序順序的。
如上圖,一筆訂單中共使用到了4種優惠,可以說我們組合使用了四種優惠策略。
我們先拿點外賣中會員折扣活動舉例子來說明一下吧。外賣平臺上的某家店鋪為了促銷,設定了多種會員優惠,其中包含超級會員折扣8折、普通會員折扣9折和普通使用者沒有折扣三種。
我們希望使用者在付款的時候,根據使用者的會員等級,就可以知道使用者符合哪種折扣策略,進而進行打折,計算出應付金額。
程式碼中可以這樣寫:
public BigDecimal calPrice(BigDecimal orderPrice, String buyerType) {
if (BuyerType.SUPER_VIP.name().equals(buyerType)) {
return orderPrice.multiply(new BigDecimal(0.8));
}
if (BuyerType.VIP.name().equals(buyerType)) {
return orderPrice.multiply(new BigDecimal(0.9));
}
return orderPrice;
}
複製程式碼
以上程式碼比較簡單,就是在程式碼中通過if-else進行邏輯判斷,不同型別的會員享受不同的折扣價。
再增加一種會員型別
這個時候,平臺增加了一種店鋪專屬會員,這種會員可以專享某一個店鋪菜品的7折優惠,那麼程式碼又要改成以下這樣:
public BigDecimal calPrice(BigDecimal orderPrice, String buyerType) {
if (BuyerType.PARTICULARLY_VIP.name().equals(buyerType)) {
return orderPrice.multiply(new BigDecimal(0.7));
}
if (BuyerType.SUPER_VIP.name().equals(buyerType)) {
return orderPrice.multiply(new BigDecimal(0.8));
}
if (BuyerType.VIP.name().equals(buyerType)) {
return orderPrice.multiply(new BigDecimal(0.9));
}
return orderPrice;
}
複製程式碼
會員折扣變化
後面,隨著業務發展,新的需求要求專屬會員要在店鋪下單金額大於30元的時候才可以享受優惠。程式碼就需要再次修改:
public BigDecimal calPrice(BigDecimal orderPrice, String buyerType) {
if (BuyerType.PARTICULARLY_VIP.name().equals(buyerType)) {
if (orderPrice.compareTo(new BigDecimal(30)) > 0) {
return orderPrice.multiply(new BigDecimal(0.7));
}
}
if (BuyerType.SUPER_VIP.name().equals(buyerType)) {
return orderPrice.multiply(new BigDecimal(0.8));
}
if (BuyerType.VIP.name().equals(buyerType)) {
return orderPrice.multiply(new BigDecimal(0.9));
}
return orderPrice;
}
複製程式碼
接著,又有一個變態的需求,如果使用者的超級會員已經到期了,並且到期時間在一週內,那麼就對使用者的單筆訂單按照超級會員進行折扣,並在收銀臺進行強提醒,引導使用者再次開通會員,而且折扣只進行一次。程式碼需要做如下修改:
public BigDecimal calPrice(BigDecimal orderPrice, String buyerType) {
if (BuyerType.PARTICULARLY_VIP.name().equals(buyerType)) {
if (orderPrice.compareTo(new BigDecimal(30)) > 0) {
return orderPrice.multiply(new BigDecimal(0.7));
}
}
if (BuyerType.SUPER_VIP.name().equals(buyerType)) {
return orderPrice.multiply(new BigDecimal(0.8));
}
if (BuyerType.VIP.name().equals(buyerType)) {
int superVipExpiredDays = getSuperVipExpiredDays();
int superVipLeadDiscountTimes = getSuperVipLeadDiscountTimes();
if(superVipExpiredDays < 7 && superVipLeadDiscountTimes =0){
updateSuperVipLeadDiscountTimes();
return orderPrice.multiply(new BigDecimal(0.8));
}
return orderPrice.multiply(new BigDecimal(0.9));
}
return orderPrice;
}
複製程式碼
以上程式碼,所有關於會員折扣的程式碼全部都寫在了一個calPrice方法中,增加或者減少一種會員型別,就需要改動到整個方法。還要考慮這種會員折扣的優先順序問題。
除了增加會員型別外,其中任何一種會員型別的折扣策略發生變化,也需要改動到整個演算法。
這就會導致這個演算法越來越臃腫,進而得到的後果就是程式碼行數越來越多,開發人員改動一點點程式碼都需要把所有功能全部都回歸一遍。
就比如說我只是把超級會員的折扣從8折改為8.5折,這時候因為程式碼都在一起,那就需要在上線的時候對於會員折扣的所有功能進行迴歸。
我們說日常生活中,我們要實現目標,有很多方案,每一個方案都被稱之為一個策略。在軟體開發中也常常遇到類似的情況,實現某一個功能有多個途徑,此時可以使用一種設計模式來使得系統可以靈活地選擇解決途徑,也能夠方便地增加新的解決途徑。這就是策略模式。
策略模式(Strategy Pattern),指的是定義一系列演算法,將每一個演算法封裝起來,並讓它們可以相互替換。策略模式讓演算法獨立於使用它的客戶而變化。
特別說明一下,策略模式只適用管理一組同型別的演算法,並且這些演算法是完全互斥的情況。也就是說任何時候,多個策略中只有一個可以生效的那一種。如滿減中的滿20減10與滿30減20之間;普通會員折扣與超級會員折扣之間等。
在策略模式中,定義一些獨立的類來封裝不同的演算法,每一個類封裝一個具體的演算法,在這裡,每一個封裝演算法的類我們都可以稱之為策略(Strategy),為了保證這些策略的一致性,一般會用一個抽象的策略類來做演算法的定義,而具體每種演算法則對應於一個具體策略類。
要實現策略模式,肯定離不開策略。如前面提到的超級會員、普通會員、專屬會員等的折扣其實都是策略。完全可以通過策略模式來實現。
實現策略模式主要包含的角色如下:
抽象策略類
先定義一個介面,這個介面就是抽象策略類,該介面定義了計算價格方法,具體實現方式由具體的策略類來定義。
public interface Buyer {
/**
* 計算應付價格
*/
public BigDecimal calPrice(BigDecimal orderPrice);
}
複製程式碼
具體策略類
針對不同的會員,定義三種具體的策略類,每個類中都分別實現計算價格方法。
/**
* 專屬會員
*/
public class ParticularlyVipBuyer implements Buyer {
@Override
public BigDecimal calPrice(BigDecimal orderPrice) {
if (orderPrice.compareTo(new BigDecimal(30)) > 0) {
return orderPrice.multiply(new BigDecimal(0.7));
}
}
}
/**
* 超級會員
*/
public class SuperVipBuyer implements Buyer {
@Override
public BigDecimal calPrice(BigDecimal orderPrice) {
return orderPrice.multiply(new BigDecimal(0.8));
}
}
/**
* 普通會員
*/
public class VipBuyer implements Buyer {
@Override
public BigDecimal calPrice(BigDecimal orderPrice) {
int superVipExpiredDays = getSuperVipExpiredDays();
int superVipLeadDiscountTimes = getSuperVipLeadDiscountTimes();
if(superVipExpiredDays < 7 && superVipLeadDiscountTimes =0){
return orderPrice.multiply(new BigDecimal(0.8));
}
return orderPrice.multiply(new BigDecimal(0.9));
}
}
複製程式碼
上面幾個類的定義體現了封裝變化的設計原則,不同會員的具體折扣方式改變不會影響到其他的會員。
定義好了抽象策略類和具體策略類之後,我們再來定義上下文類,所謂上下文類,就是整合演算法的類。這個例子中就是收銀臺系統。採用組合的方式把會員整合進來。
public class Cashier {
/**
* 會員,策略物件
*/
private Buyer buyer;
public Cashier(Buyer buyer){
buyer = buyer;
}
public BigDecimal quote(BigDecimal orderPrice) {
return this.buyer.calPrice(orderPrice);
}
}
複製程式碼
這個Cashier類就是一個上下文類,該類的定義體現了多用組合,少用繼承、針對介面程式設計,不針對實現程式設計兩個設計原則。
由於這裡採用了組合+介面的方式,後面我們在推出其他型別會員的時候無須修改Cashier類。只要再定義一個類實現Buyer介面 就可以了。
除了增加會員型別以外,我們想要修改某個會員的折扣情況的時候,只需要修改該會員對應的策略類就可以了,不需要修改到其他的策略。也就控制了變更的範圍。大大降低了成本。
下面定義一個客戶端來測試一下:
public class Test {
public static void main(String[] args) {
//選擇並建立需要使用的策略物件
Buyer strategy = new VipBuyer();
//建立上下文
Cashier cashier = new Cashier(strategy);
//計算價格
BigDecimal quote = cashier.quote(300);
System.out.println("普通會員商品的最終價格為:" + quote.doubleValue());
strategy = new SuperVipBuyer();
cashier = new Cashier(strategy);
quote = cashier.quote(300);
System.out.println("超級會員商品的最終價格為:" + quote.doubleValue());
}
}
複製程式碼
輸出結果:
//普通會員商品的最終價格為:270.0
//超級會員商品的最終價格為:240.0
複製程式碼
從上面的示例可以看出,策略模式僅僅封裝演算法,提供新的演算法插入到已有系統中,策略模式並不決定在何時使用何種演算法。在什麼情況下使用什麼演算法是由客戶端決定的。
策略模式可以充分的體現物件導向設計原則中的封裝變化、多用組合,少用繼承、針對介面程式設計,不針對實現程式設計等原則。
策略模式具有以下特點:
策略模式的關注點不是如何實現演算法,而是如何組織、呼叫這些演算法,從而讓程式結構更靈活,具有更好的維護性和擴充套件性。
策略模式中各個策略演算法是平等的。對於一系列具體的策略演算法,大家的地位是完全一樣的,正因為這個平等性,才能實現演算法之間可以相互替換。所有的策略演算法在實現上也是相互獨立的,相互之間是沒有依賴的。所以可以這樣描述這一系列策略演算法:策略演算法是相同行為的不同實現。
執行期間,策略模式在每一個時刻只能使用一個具體的策略實現物件,雖然可以動態地在不同的策略實現中切換,但是同時只能使用一個。
如果所有的具體策略類都有一些公有的行為。這時候,就應當把這些公有的行為放到共同的抽象策略角色Strategy類裡面。當然這時候抽象策略角色必須要用Java抽象類實現,而不能使用介面。
但是,程式設計中沒有銀彈,策略模式也不例外,他也有一些缺點,我們先來回顧總結下他的優點:
策略模式提供了對“開閉原則”的完美支援,使用者可以在不修改原有系統的基礎上選擇演算法或行為,也可以靈活地增加新的演算法或行為。
策略模式提供了管理相關的演算法族的辦法。策略類的等級結構定義了一個演算法或行為族。恰當使用繼承可以把公共的程式碼移到父類裡面,從而避免程式碼重複。
使用策略模式可以避免使用多重條件(if-else)語句。多重條件語句不易維護,它把採取哪一種演算法或採取哪一種行為的邏輯與演算法或行為的邏輯混合在一起,統統列在一個多重條件語句裡面,比使用繼承的辦法還要原始和落後。
但同時,他也有如下缺點:
客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。這就意味著客戶端必須理解這些演算法的區別,以便適時選擇恰當的演算法類。這種策略類的建立及選擇其實也可以通過工廠模式來輔助進行。
由於策略模式把每個具體的策略實現都單獨封裝成為類,如果備選的策略很多的話,那麼物件的數目就會很可觀。可以通過使用享元模式在一定程度上減少物件的數量。
最後,附上本文內容的思維導圖: