設(shè)計(jì)模式初探-行為型模式之模版方法模式

滿(mǎn)堂花醉三千客迁沫,一劍霜寒十四州芦瘾。在并發(fā)編程之AQS探秘中捌蚊,我們有提到AbstractQueuedSynchroizer是基于模板方法模式設(shè)計(jì)的,那么到底什么是模板方法模式近弟?運(yùn)行的原理又是什么缅糟?下面就讓我們帶著這些問(wèn)題一探究竟。本文包括以下部分:

  1. 前言
  2. 模板方法模式
    2.1 為何要用
    2.2 定義
    2.3 一個(gè)例子-奶茶
    2.4 添加鉤子
    2.5 JDK中的模板方法
    2.6 模板方法VS策略模式VS工廠(chǎng)方法
  3. 總結(jié)

1. 前言

面向?qū)ο笫澜缋锏娜筇匦浴?strong>封裝藐吮、繼承溺拱、多態(tài)盛名在外逃贝,無(wú)人不知無(wú)人不曉谣辞。設(shè)計(jì)模式往往也是圍繞著這些特性展開(kāi)的,就拿封裝來(lái)說(shuō)沐扳,工廠(chǎng)模式封裝了對(duì)象的創(chuàng)建泥从、命令模式封裝了方法的調(diào)用、門(mén)面模式封裝了復(fù)雜接口沪摄。接下來(lái)我們要封裝算法躯嫉。


2. 模板方法模式

2.1 為什么要用

當(dāng)我們用一種新東西的時(shí)候,著急去使用之前杨拐,不妨花一點(diǎn)時(shí)間想一想為什么要用祈餐,當(dāng)我們想明白為什么要用后。才會(huì)讓我們?cè)诮酉聛?lái)的運(yùn)用中更加得心應(yīng)手哄陶。

假設(shè)作者本人是個(gè)大廚??帆阳,大廚現(xiàn)在要烹飪兩個(gè)菜:紅燒肉和糖醋排骨。
其中紅燒肉的制作過(guò)程大致如下:

  1. 準(zhǔn)備五花肉
  2. 倒入炒鍋
  3. 翻炒
  4. 添加醬油
  5. 裝盤(pán)

糖醋排骨的制作過(guò)程大致如下:

  1. 準(zhǔn)備排骨
  2. 倒入炒鍋
  3. 翻炒
  4. 添加糖
  5. 裝盤(pán)
  • 第一版代碼如下
package com.moxieliunian.template;
//紅燒肉
public class BraisedPork {
    //食物制作
    void prepareFood(){
        //準(zhǔn)備五花肉
        preparePork();
        //倒入炒鍋
        putInPan();
        //翻炒
        fry();
        //添加醬油
        addSoy();
        //裝盤(pán)出鍋
        fill();
    }


    void preparePork(){
        System.out.println("準(zhǔn)備紅燒肉");
    }

    void putInPan(){
        System.out.println("倒入鍋中");
    }

    void fry(){
        System.out.println("翻炒");
    }

    void addSoy(){
        System.out.println("添加醬油");
    }

    void fill(){
        System.out.println("裝盤(pán)屋吨,制作完畢");
    }
}
package com.moxieliunian.template;
//糖醋排骨
public class Ribs {
    //食物制作
    void prepareFood(){
        //準(zhǔn)備五花肉
        prepareRibs();
        //倒入炒鍋
        putInPan();
        //翻炒
        fry();
        //添加糖
        addSugar();
        //裝盤(pán)出鍋
        fill();
    }


    void prepareRibs(){
        System.out.println("準(zhǔn)備排骨");
    }

    void putInPan(){
        System.out.println("倒入鍋中");
    }

    void fry(){
        System.out.println("翻炒");
    }

    void addSugar(){
        System.out.println("添加糖");
    }

    void fill(){
        System.out.println("裝盤(pán)蜒谤,制作完畢");
    }
}

我們發(fā)現(xiàn)了這兩個(gè)類(lèi)中大量的重復(fù)代碼。既然有重復(fù)的代碼至扰,這表示我們可以重新整理下設(shè)計(jì)鳍徽。那么如何做呢?很自然的想法就是把公共的代碼提取出來(lái)敢课,放到一個(gè)基類(lèi)中阶祭。

  • 第二版代碼如下
public abstract class FoodBase {
   abstract void prepareFood();
    void putInPan(){
        System.out.println("倒入鍋中");
    }

    void fry(){
        System.out.println("翻炒");
    }

    void fill(){
        System.out.println("裝盤(pán),制作完畢");
    }
}
//紅燒肉
public class BraisedPork extends FoodBase {
    @Override
    void prepareFood() {
        //準(zhǔn)備五花肉
        preparePork();
        //倒入炒鍋
        putInPan();
        //翻炒
        fry();
        //添加醬油
        addSoy();
        //裝盤(pán)出鍋
        fill();
    }

    private void preparePork() {
        System.out.println("準(zhǔn)備紅燒肉");
    }

    private void addSoy() {
        System.out.println("添加醬油");
    }
}
//糖醋排骨
public class Ribs extends FoodBase {
    @Override
        //食物制作
    void prepareFood() {
        //準(zhǔn)備五花肉
        prepareRibs();
        //倒入炒鍋
        putInPan();
        //翻炒
        fry();
        //添加糖
        addSugar();
        //裝盤(pán)出鍋
        fill();
    }


    private void prepareRibs() {
        System.out.println("準(zhǔn)備排骨");
    }


    private void addSugar() {
        System.out.println("添加糖");
    }
}
紅燒肉直秆、糖醋排骨第二版類(lèi)圖.png

此時(shí)我們發(fā)現(xiàn)濒募,算法的實(shí)現(xiàn)流程由子類(lèi)控制,那么紅燒肉和糖醋排骨還有什么共通點(diǎn)嗎切厘?
preparePork和prepareRibs萨咳,addSoy和addSugar 只是作用的對(duì)象不一樣,但是具體的作用都是相同的疫稿。我們嘗試做第三版抽象培他。

  • 第三版代碼
public abstract class FoodBase {
    //規(guī)定了食物的制作流程
    protected void prepareFood() {
        //準(zhǔn)備原材料
        prepareMaterial();
        //倒入炒鍋
        putInPan();
        //翻炒
        fry();
        //添加配料
        addIngredient();
        //裝盤(pán)出鍋
        fill();
    }

    //準(zhǔn)備原材料
    protected abstract void prepareMaterial();

    //添加配料
    protected abstract void addIngredient();

    private void putInPan() {
        System.out.println("倒入鍋中");
    }

    private void fry() {
        System.out.println("翻炒");
    }

    private void fill() {
        System.out.println("裝盤(pán)鹃两,制作完畢");
    }
}
//紅燒肉
public class BraisedPork extends FoodBase {
    @Override
    protected void prepareMaterial() {
        System.out.println("準(zhǔn)備紅燒肉");
    }

    @Override
    protected void addIngredient() {
        System.out.println("添加醬油");
    }

}
//糖醋排骨
public class Ribs extends FoodBase {
    @Override
    protected void prepareMaterial() {
        System.out.println("準(zhǔn)備排骨");
    }

    @Override
    protected void addIngredient() {
        System.out.println("添加糖");
    }
}

我們將preparePork和prepareRibs,addSoy和addSugar 抽象為prepareMaterial和addIngredient舀凛,此時(shí)類(lèi)圖如下:

紅燒肉俊扳、糖醋排骨第三版類(lèi)圖.png

問(wèn):第三版代碼(模板方法)相比前兩版而言,有什么好處猛遍?
1. 算法的骨架由基類(lèi)(FoodBase)規(guī)定且保護(hù)馋记,且只存在基類(lèi)中,后期修改只需要修改一處即可懊烤,便于維護(hù)梯醒。2. 子類(lèi)只需要覆蓋其需要覆蓋的方法,可以達(dá)到最大化的代碼復(fù)用腌紧。3. 同時(shí)可以時(shí)間算法的實(shí)現(xiàn)和算法本身想分離

2.2 定義

明白了上面的例子茸习,我們就了解了模板方法的原理。模板方法在一個(gè)方法中定義了算法的骨架壁肋,而將一些步驟延遲到子類(lèi)中号胚。模板方法使得子類(lèi)可以在不改變算法結(jié)構(gòu)的情況下,重新定義算法中的某些步驟浸遗。

模板方法類(lèi)圖.png

就拿我們上面的例子來(lái)說(shuō)猫胁,模板方法FoodBase.prepareFood()規(guī)定了食物的制作流程為:準(zhǔn)備原料、倒入炒鍋跛锌、翻炒弃秆、添加配料媒峡、出鍋抵卫。子類(lèi)只需要實(shí)現(xiàn)準(zhǔn)備原料和添加配料的方法即可。所有的子類(lèi)食物制作方法都是按照這個(gè)流程進(jìn)行事格。

2.3 一個(gè)例子-奶茶

現(xiàn)在有個(gè)奶茶店氢卡,生產(chǎn)兩種奶茶:珍珠奶茶和紅豆奶茶,其中珍珠奶茶制作流程如下:

  • 準(zhǔn)備奶和茶
  • 倒入杯子
  • 添加珍珠
  • 加冰
  • 密封

而紅豆奶茶制作如下

  • 準(zhǔn)備奶和茶
  • 倒入杯子
  • 添加紅豆
  • 加冰
  • 密封

這兩個(gè)過(guò)程有著相同的算法骨架锈至,我們很容易想到用模板方法模式來(lái)實(shí)現(xiàn),如下:

//奶茶算法基類(lèi)
public abstract class TeaBase {
    protected void prdouceTea(){
        //準(zhǔn)備奶和茶
        prepareMilkAndTea();
        //倒入杯子
        putInCup();
        //添加配料
        addIngredient();
        //加冰
        addIce();
        //密封打包
        box();
    }

    private void prepareMilkAndTea(){
        System.out.println("準(zhǔn)備奶和茶");
    }

    private void  putInCup(){
        System.out.println("倒入杯子");
    }

    protected abstract void addIngredient();

    private void addIce(){
        System.out.println("加冰");
    }

    private void box(){
        System.out.println("密封");
    }

}
//珍珠奶茶
public class BubbleTea extends TeaBase{
    @Override
    protected void addIngredient() {
        System.out.println("添加珍珠");
    }
}
//紅豆奶茶
public class RedBeanTea extends TeaBase{
    @Override
    protected void addIngredient() {
        System.out.println("添加紅豆");
    }
}

使用我們的算法

        BubbleTea bubbleTea=new BubbleTea();
        RedBeanTea redBeanTea=new RedBeanTea();
        System.out.println("珍珠奶茶制作開(kāi)始");
        bubbleTea.prdouceTea();
        System.out.println("珍珠奶茶制作結(jié)束");
        System.out.println("紅豆奶茶制作開(kāi)始");
        redBeanTea.prdouceTea();
        System.out.println("紅豆奶茶制作結(jié)束");
珍珠奶茶制作開(kāi)始
準(zhǔn)備奶和茶
倒入杯子
添加珍珠
加冰
密封
珍珠奶茶制作結(jié)束
紅豆奶茶制作開(kāi)始
準(zhǔn)備奶和茶
倒入杯子
添加紅豆
加冰
密封
紅豆奶茶制作結(jié)束

可以看到算法按照我們規(guī)定的骨架译秦,正確執(zhí)行了

2.4 添加鉤子

上面的例子中峡捡,我們使用模板方法模式來(lái)實(shí)現(xiàn)了奶茶的制作流程。但是有一個(gè)問(wèn)題筑悴,無(wú)論是珍珠奶茶還是紅豆奶茶们拙,都默認(rèn)加冰。如果要制作一杯不加冰的奶茶該如何操作呢阁吝?我們可以添加一個(gè)鉤子,Hook砚婆。先看怎么用。

//奶茶算法基類(lèi)
public abstract class TeaBase {
    protected void prdouceTea(){
        //準(zhǔn)備奶和茶
        prepareMilkAndTea();
        //倒入杯子
        putInCup();
        //添加配料
        addIngredient();
        //默認(rèn)加冰
        if (isNeedIce()){
            //加冰
            addIce();
        }
        //密封打包
        box();
    }

    private void prepareMilkAndTea(){
        System.out.println("準(zhǔn)備奶和茶");
    }

    private void  putInCup(){
        System.out.println("倒入杯子");
    }

    protected abstract void addIngredient();
    //是否加冰突勇,默認(rèn)是
    protected boolean isNeedIce(){
        return true;
    }

    private void addIce(){
        System.out.println("加冰");
    }

    private void box(){
        System.out.println("密封");
    }

}
//由客戶(hù)自己決定是否加冰的珍珠奶茶
public class SmartBubbleTea extends TeaBase{
    private boolean isNeedIce;
    SmartBubbleTea(boolean isNeedIce){
        super();
        this.isNeedIce=isNeedIce;
    }
    @Override
    protected void addIngredient() {
        System.out.println("添加珍珠");
    }

    @Override
    public boolean isNeedIce() {
        return isNeedIce;
    }
}

使用

        SmartBubbleTea smartBubbleTea = new SmartBubbleTea(false);
        System.out.println("不加冰珍珠奶茶制作開(kāi)始");
        smartBubbleTea.prdouceTea();
        System.out.println("不加冰珍珠奶茶制作結(jié)束");
不加冰的珍珠奶茶制作開(kāi)始
準(zhǔn)備奶和茶
倒入杯子
添加珍珠
密封
不加冰的珍珠奶茶制作結(jié)束

可以看到装盯,我們利用hook成功實(shí)現(xiàn)了坷虑,奶茶加不加冰的自由控制。

hook的作用有以下幾點(diǎn):

  • 鉤子可以讓子類(lèi)實(shí)現(xiàn)算法的可選部分埂奈,或者在鉤子對(duì)于子類(lèi)的實(shí)現(xiàn)不重要的時(shí)候迄损,子類(lèi)可以對(duì)這個(gè)鉤子置之不理
  • 鉤子可以讓子類(lèi)對(duì)模板中即將發(fā)生的東西做出反應(yīng)账磺。

2.5 JDK中的模板方法

上面說(shuō)了那么多芹敌,都是我們自己在寫(xiě)demo。那么JDK中有沒(méi)有用到模板方法的地方呢垮抗?當(dāng)然是有的氏捞,比如:AbstractQueuedSynchronizer.acquire(int arg)、AbstractList.addAll(int index, Collection<? extends E> c)借宵、Arrays.sort(Object[] a)等幌衣,以Arrays.sort為例,找出其中的模板方法

       public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a);
        else
            ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
    }
    static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
        assert a != null && lo >= 0 && lo <= hi && hi <= a.length;

        int nRemaining  = hi - lo;
        if (nRemaining < 2)
            return;  // Arrays of size 0 and 1 are always sorted

        // If array is small, do a "mini-TimSort" with no merges
        if (nRemaining < MIN_MERGE) {
            int initRunLen = countRunAndMakeAscending(a, lo, hi);
            binarySort(a, lo, hi, lo + initRunLen);
            return;
        }
private static void binarySort(Object[] a, int lo, int hi, int start) {
        assert lo <= start && start <= hi;
        if (start == lo)
            start++;
        for ( ; start < hi; start++) {
            Comparable pivot = (Comparable) a[start];

            // Set left (and right) to the index where a[start] (pivot) belongs
            int left = lo;
            int right = start;
            assert left <= right;
            /*
             * Invariants:
             *   pivot >= all in [lo, left).
             *   pivot <  all in [right, start).
             */
            while (left < right) {
                int mid = (left + right) >>> 1;
                if (pivot.compareTo(a[mid]) < 0)
                    right = mid;
                else
                    left = mid + 1;
            }
            assert left == right;

可以看到壤玫,sort方法調(diào)用的時(shí)候,依賴(lài)于傳入對(duì)象的compareTo方法哼凯,也就是說(shuō)sort方法規(guī)定了排序的執(zhí)行骨架欲间,而由具體的類(lèi)去決定排序的算法細(xì)節(jié)(重寫(xiě)compareTo)。這里之沒(méi)有用到繼承断部,一方面是因?yàn)閿?shù)組不能被繼承猎贴,另一方面則是因?yàn)榕判蚍椒ㄏM茏饔糜谒械臄?shù)組

我們可以看出設(shè)計(jì)模式往往并不是一塵不變的蝴光,使用中往往伴隨著不同場(chǎng)景的適配她渴。

2.6 模板方法VS策略模式VS工廠(chǎng)方法

模板方法模式和策略模式都是跟算法有關(guān),那么這兩個(gè)設(shè)計(jì)模式之間有什么異同呢蔑祟?模板方法模式和工廠(chǎng)方法模式又有什么異同呢趁耗?

名稱(chēng) 定義 區(qū)別與聯(lián)系
策略模式 定義了算法族,分別封裝起來(lái)疆虚,讓它們之間可以互換苛败。 與模板方法一樣都是作用于算法,不同的是策略模式側(cè)重于算法的替換径簿,使用組合來(lái)實(shí)現(xiàn)罢屈。
模板方法模式 在一個(gè)方法中定義了算法的骨架,將某些實(shí)現(xiàn)推遲到子類(lèi)中篇亭,使子類(lèi)可以改變實(shí)現(xiàn)細(xì)節(jié)缠捌,而不影響整體架構(gòu) 與策略模式一樣,都是作用于算法译蒂。不同的是模板方法側(cè)重于子類(lèi)改變算法的某個(gè)細(xì)節(jié)曼月,而不會(huì)改變模板的整個(gè)算法骨架肃叶,使用繼承實(shí)現(xiàn)。
工廠(chǎng)方法模式 定義了一個(gè)創(chuàng)建對(duì)象的接口十嘿,但由子類(lèi)決定要實(shí)例化的是哪一個(gè)因惭,將類(lèi)的實(shí)例化推遲到子類(lèi)中 工廠(chǎng)方法是模板方的一個(gè)特殊版本

3. 總結(jié)

本篇文章中,我們探討了模板方法模式的相關(guān)內(nèi)容绩衷。并和與之相似的設(shè)計(jì)模式:策略模式蹦魔、工廠(chǎng)方法模式進(jìn)行了簡(jiǎn)單對(duì)比。

由于技術(shù)水平所限咳燕,文章難免有不足之處勿决,歡迎大家指出。我們下一篇文章見(jiàn).....

參考文章
head first 設(shè)計(jì)模式

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末招盲,一起剝皮案震驚了整個(gè)濱河市低缩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌曹货,老刑警劉巖咆繁,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異顶籽,居然都是意外死亡玩般,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)礼饱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)坏为,“玉大人,你說(shuō)我怎么就攤上這事镊绪≡确” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵蝴韭,是天一觀的道長(zhǎng)够颠。 經(jīng)常有香客問(wèn)我,道長(zhǎng)万皿,這世上最難降的妖魔是什么摧找? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮牢硅,結(jié)果婚禮上蹬耘,老公的妹妹穿的比我還像新娘。我一直安慰自己减余,他們只是感情好综苔,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般如筛。 火紅的嫁衣襯著肌膚如雪堡牡。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天杨刨,我揣著相機(jī)與錄音晤柄,去河邊找鬼。 笑死妖胀,一個(gè)胖子當(dāng)著我的面吹牛芥颈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播赚抡,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼爬坑,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了涂臣?” 一聲冷哼從身側(cè)響起盾计,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎赁遗,沒(méi)想到半個(gè)月后署辉,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吼和,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年涨薪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炫乓。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖献丑,靈堂內(nèi)的尸體忽然破棺而出末捣,到底是詐尸還是另有隱情,我是刑警寧澤创橄,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布箩做,位于F島的核電站,受9級(jí)特大地震影響妥畏,放射性物質(zhì)發(fā)生泄漏邦邦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一醉蚁、第九天 我趴在偏房一處隱蔽的房頂上張望燃辖。 院中可真熱鬧,春花似錦网棍、人聲如沸黔龟。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)氏身。三九已至巍棱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蛋欣,已是汗流浹背航徙。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留陷虎,地道東北人到踏。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像泻红,于是被迫代替她去往敵國(guó)和親夭禽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容