1. 程式人生 > >策略模式(Strategy Pattern)就這麼簡單

策略模式(Strategy Pattern)就這麼簡單

0x00 舉個生活中的例子

某個大人網站是會員制的,金牌會員購買精神食糧打7折,銀牌會員打8折,銅牌會員打9折,鐵牌會員不打折。也就是說不同的使用者在購買精神食糧的時候結算的價格是不一樣的,即使你們買相同公司出品的相同食糧,你們的總價格是不一樣的,因為根據會員等級不同,有不同的折扣。

也就是說,面對不同會員等級的使用者,大人網站會有不同的計價演算法。

問過度孃的人都知道,假如你經常問度娘哪裡有可堪一擼的資源,此後你瀏覽不同網站時,給你推薦的廣告都是衣服比較少的妹子圖,也許你還會想這網站蠻懂我的嘛。如果你經常問度娘哪位老中醫比較厲害,以後你上網的時候,網站給你推薦的廣告可能就是如何治療早洩之類的(不要問我為什麼知道)。

也就是說,面對不同的使用者,度孃的廣告系統或谷歌的廣告系統會使用不同的廣告推薦演算法。

這也就是所謂的策略,同樣是購買精神食糧,你的就比我的貴,因為我可是金牌會員!對映到程式設計師的世界裡,策略就是演算法了,策略模式就是處理同一件事,我可以有好幾個不同的策略(演算法)。

0x01 沒事玩個雞應用

汙汙公司開發了個應用叫沒事玩個雞,這是一款娛樂類應用,可以一鍵玩雞,有不同品類的雞給你玩,如三黃雞、烏雞、白雞等。這個應用的內部設計是標準面向物件技術,設計了一個雞超類(Superclass),讓各種雞繼承此超類。我們來看一下程式碼:

/**
 * 超類雞
 */
abstract class
Chicken {
/** * 打招呼 */ public void sayHi() { System.out.println("咯咯咯~"); } /** * 雞的樣子,每種品類的雞樣子都不一樣,所以該方法是抽象的 * 由具體的雞來實現自己在螢幕上顯示的樣子 */ public abstract void show(); } /** * 白雞,繼承Chicken類 */ class WhiteChicken extends Chicken { @Override public void show() { // 樣子是白色的
} } /** * 烏雞,繼承Chicken類 */ class BlackChicken extends Chicken { @Override public void show() { // 樣子是烏黑的 } } /** * 尖叫雞,繼承Chicken類 */ class ScreamChicken extends Chicken { /** * 尖叫雞不會咯咯叫,所以重寫sayHi()方法 */ @Override public void sayHi() { System.out.println("慘叫聲~"); } @Override public void show() { // 樣子是無毛的 } }

程式碼很簡單,具體品類的雞繼承超類(父類)雞,所有的雞都會叫,所以由父類處理。不同品類的雞樣子不同,所以由具體品類的雞來實現。至於尖叫雞是長這樣的:

尖叫雞

沒事玩個雞應用很快火了,出現了很多競爭對手了,這時產品經理想要改變一些玩法來拋開競爭對手,想出了讓雞可以跑動,這樣使用者可就以玩跑動中的雞了,程式設計師一想,這簡單嘛,只需要在父類中加一個run()方法就可以了,這樣所有品類的雞都會跑了:

/**
* 跑步
*/
public void run() {
    System.out.println("拼命跑");
}

改完後就交給測試人員去測試去了。測試人員一測試,天了擼~出了個明顯的bug啊,尖叫雞也會跑!於是將bug提交到了系統中。

程式設計師一看,這確實不應該讓尖叫雞也能跑,先修復再說,於是他這樣改了尖叫雞類:

/**
 * 尖叫雞,繼承Chicken類
 */
class ScreamChicken extends Chicken {

    /**
     * 尖叫雞不會咯咯叫,所以重寫sayHi()方法
     */
    @Override
    public void sayHi() {
        System.out.println("慘叫聲~");
    }

    @Override
    public void run() {
        // 覆蓋,什麼也不做
    }

    @Override
    public void show() {
        // 樣子是無毛的
    }
}

程式設計師重寫了run()方法,然後什麼也不實現,這樣修復了bug。通過這個bug程式設計師也體會到了一件事:當涉及“維護”時,為了”複用(reuse)”目的而使用繼承,並不太完美。

沒事玩個雞應用更新後更火了,於是產品經理決定每個月更新一次產品(至於更新的方法,他們還沒有想到)。

程式設計師接到產品經理的更新計劃就想,以後萬一又要增加一些品類的雞會怎樣?萬一有的功能部分品類的雞是不具備的呢?那不是又得改父類又得改子類,牽一髮而動全身,看來需要一個更好的方式才行。於是他開始翻看程式設計指南,終於找到一個設計原則。

設計原則

找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的程式碼混在一起。

換句話說,如果每次新的需求一來,都會使某方面的程式碼發生變化,那你就可以確定,這部分程式碼需要被抽出來,和其他穩定的程式碼有所區分,把變化的部分封裝起來,以便以後可以輕易的改動或擴充套件此部分,而不影響不需要變化的部分。

0x02 分開變化和不會變化的部分

從哪裡開始呢?我們知道Chicken類的sayHi()方法和run()方法會隨著雞的不同而改變,比如尖叫雞sayHi()的方式就和別的雞不一樣,它也不會跑。根據上面說的設計原則,需要將它們獨立出來,為了把這兩個行為從Chicken類中分開,程式設計師將它們從Chicken類中取出來,建立一組新類來代表每個行為。示意圖如下:

取出改變部分

現在已經把變化和不會變化的部分分開了,需要考慮如何設計雞的行為類了。繼續翻看程式設計指南,發現了另一個設計原則。

設計原則

針對介面程式設計,而不是針對實現程式設計。

現在程式設計師利用介面代表每個行為,比如,SayHiBehaviorRunBehavior,而行為的每個實現都將實現其中的一個介面。我們來看一下這兩個介面的程式碼:

  • SayHiBehavior介面:
interface SayHiBehavior {
    void sayHi();
}
  • RunBehavior介面:
interface RunBehavior {
    void run();
}

介面定義好了,我們就可以弄一些具體實現了。

  • 普通打招呼:
/**
 * 普通打招呼
 */
class SayHiNormally implements SayHiBehavior {

    @Override
    public void sayHi() {
        System.out.println("咯咯咯~");
    }
}
  • 尖叫打招呼
/**
 * 尖叫打招呼
 */
class SayHiScream implements SayHiBehavior {

    @Override
    public void sayHi() {
        System.out.println("慘叫聲~");
    }
}
  • 正常跑步
/**
 * 正常跑
 */
class RunNormally implements RunBehavior {

    @Override
    public void run() {
        System.out.println("拼命跑");
    }
}
  • 不會跑步
/**
 * 不會跑
 */
class RunNoWay implements RunBehavior {

    @Override
    public void run() {
        // 什麼也不做,不會跑
    }
}

一切準備就緒,我們該開始重構了。

0x03 重構沒事玩個雞應用

父類Chicken重構如下:

/**
 * 超類雞
 */
abstract class Chicken {
//    /**
//     * 打招呼
//     */
//    public void sayHi() {
//        System.out.println("咯咯咯~");
//    }
//
//    /**
//     * 跑步
//     */
//    public void run() {
//        System.out.println("拼命跑");
//    }
    // 打招呼行為
    private SayHiBehavior mSayHiBehavior;
    // 跑步行為
    private RunBehavior mRunBehavior;

    /**
     * 設定打招呼行為
     * @param sayHiBehavior
     */
    public void setSayHiBehavior(SayHiBehavior sayHiBehavior) {
        mSayHiBehavior = sayHiBehavior;
    }

    /**
     * 設定跑步行為
     * @param runBehavior
     */
    public void setRunBehavior(RunBehavior runBehavior) {
        mRunBehavior = runBehavior;
    }

    /**
     * 執行打招呼,這個方法替換之前的sayHi()方法
     */
    public void performSayHi() {
        // 委託給打招呼行為類
        mSayHiBehavior.sayHi();
    }

    /**
     * 執行跑步,這個方法替換之前的run()方法
     */
    public void performRun() {
        // 委託給跑步行為類
        mRunBehavior.run();
    }

    /**
     * 雞的樣子,每種品類的雞樣子都不一樣,所以該方法是抽象的
     * 由具體的雞來實現自己在螢幕上顯示的樣子
     */
    public abstract void show();
}

程式設計師將Chicken類的sayHi()方法和run()方法給註釋掉了,增加了打招呼行為SayHiBehavior和跑步行為RunBehavior,通過setter來設定。然後用performSayHi()performRun()方法將具體的打招呼行為和跑步行為委託給SayHiBehaviorRunBehavior去做。

測試一下,測試方法長這樣:

public class MyTest {

    public static void main(String[] args) {
        System.out.println("白雞:");
        // 白雞
        WhiteChicken whiteChicken = new WhiteChicken();
        // 設定白雞為普通打招呼行為
        whiteChicken.setSayHiBehavior(new SayHiNormally());
        // 設定白雞正常跑步行為
        whiteChicken.setRunBehavior(new RunNormally());
        // 顯示白雞
        whiteChicken.show();
        // 白雞打招呼
        whiteChicken.performSayHi();
        // 白雞打跑步
        whiteChicken.performRun();

        System.out.println("尖叫雞:");

        // 尖叫雞
        ScreamChicken screamChicken = new ScreamChicken();
        // 設定尖叫雞為慘叫打招呼行為
        screamChicken.setSayHiBehavior(new SayHiScream());
        // 設定尖叫雞不能跑步行為
        screamChicken.setRunBehavior(new RunNoWay());
        // 顯示尖叫雞
        screamChicken.show();
        // 尖叫雞打招呼
        screamChicken.performSayHi();
        // 尖叫雞跑步
        screamChicken.performRun();
    }
}

我們給白雞設定了普通打招呼策略(演算法),給尖叫雞設定的是慘叫打招呼策略(演算法),然後給白雞設定了正常跑步策略(演算法),給尖叫雞設定的是不會跑步策略(演算法)

執行結果:

result

完美的實現了策略模式,在執行時想改變雞的行為,只需要呼叫setter方法就行了。

拿出隨身攜帶的鏡子照一下,帥呆了~,叼就一個字,我說一萬次!

0x04 總結

以上就是策略模式了,定義了演算法族,分別封裝起來,讓他們之間可以互相替換。比如雞的打招呼行為就有普通打招呼和慘叫打招呼,想用哪個就用哪個。在這裡也使用了像多型組合來輔助實現策略模式。多用組合,少用繼承也是一個設計原則。

0x05 參考文獻

  • Head First 設計模式