漫話:如何給女友解釋什麼是策略模式?

算法

週末無事,窩在家裏面看《權力的遊戲第八季》,看的非常津津有味,雖然感受有一點點要爛尾,可是我仍是忍不住要去看到底誰能夠坐上鐵王座。編程

女友在一旁點外賣,好像是在使用優惠的時候遇到了一點點小問題。設計模式

策略

策略,指的是能夠實現目標的方案集合,在某些特定狀況下,策略之間是能夠相互替換的。
bash

好比咱們在外賣平臺上看到的這些優惠。滿減、會員和紅包等,每個大項優惠都具體包含了多個優惠方案。如滿減活動中,能夠同時有滿20減1五、滿50減30等。會員包含普通會員、超級會員等。ide

每個優惠方式下面的多個優惠方案,其實都是一個策略。這些策略之間是相互排斥、可替換的。而且是有必定的優先級順序的。測試

如上圖,一筆訂單中共使用到了4種優惠,能夠說咱們組合使用了四種優惠策略。ui

如何計算金額

咱們先拿點外賣中會員折扣活動舉例子來講明一下吧。外賣平臺上的某家店鋪爲了促銷,設置了多種會員優惠,其中包含超級會員折扣8折、普通會員折扣9折和普通用戶沒有折扣三種。
this

咱們但願用戶在付款的時候,根據用戶的會員等級,就能夠知道用戶符合哪一種折扣策略,進而進行打折,計算出應付金額。spa

代碼中能夠這樣寫:設計

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)語句。多重條件語句不易維護,它把採起哪種算法或採起哪種行爲的邏輯與算法或行爲的邏輯混合在一塊兒,通通列在一個多重條件語句裏面,比使用繼承的辦法還要原始和落後。

但同時,他也有以下缺點:

  • 客戶端必須知道全部的策略類,並自行決定使用哪個策略類。這就意味着客戶端必須理解這些算法的區別,以便適時選擇恰當的算法類。這種策略類的建立及選擇其實也能夠經過工廠模式來輔助進行。

  • 因爲策略模式把每一個具體的策略實現都單獨封裝成爲類,若是備選的策略不少的話,那麼對象的數目就會很可觀。能夠經過使用享元模式在必定程度上減小對象的數量。


最後,附上本文內容的思惟導圖:

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息