設(shè)計模式系列教程—Template Method Pattern(模板方法模式)

9 Template Method Pattern(模板方法模式)

前言:封裝步驟的算法矮燎。
Vander作為老板,凡是親力親為,他新開了家咖啡店舞虱,這是他招牌咖啡卡布奇諾的沖泡方法:
1、把水煮沸
2母市、用沸水沖泡咖啡
3矾兜、將咖啡倒入咖啡杯
4、加糖和奶
Vander發(fā)現(xiàn)白天喝咖啡的人實在是不多患久,白天的生意很差椅寺,白天大家都喜歡喝奶茶,特別是夏天到了蒋失,冰涼的奶茶更受歡迎配并,于是Vander開始研制它的檸檬奶茶。以下是檸檬奶茶的制作方法:
1高镐、把水煮沸
2溉旋、用沸水泡茶葉
3、將茶倒入茶杯中
4嫉髓、加檸檬和奶
Vander如行云流水般寫完了這兩個制作方法观腊。
咖啡:

public class Coffee {

    void prepareRecipe() {
        boilWater();
        brewCoffeeGrinds();
        pourInCup();
        addSugarAndMilk();
    }

    public void boilWater() {
        System.out.println("Boiling water");
    }

    public void brewCoffeeGrinds() {
        System.out.println("Dripping Coffee throught filter");
    }

    public void pourInCup() {
        System.out.println("Pouring into cup");
    }

    public void addSugarAndMilk() {
        System.out.println("Adding sugar and milk");
    }
    
}

奶茶:

public class MilkTea {

    void prepareRecipe() {
        boilWater();
        steepTeaBag();
        pourInCup();
        addTeaAndMilk();
    }

    public void boilWater() {
        System.out.println("Boiling water");
    }

    public void pourInCup() {
        System.out.println("Pouring into cup");
    }

    public void steepTeaBag() {
        System.out.println("Steeping the tea");
    }

    public void addTeaAndMilk() {
        System.out.println("Adding tea and milk");
    }
    
}

他自認(rèn)為寫得很漂亮,請來了Panda大師一起來鑒賞算行,Panda大師一看梧油,這個實現(xiàn)有太多冗余代碼了,首先奶茶跟咖啡制作流程既然是相似的州邢,為什么不做成一個Beverage抽象類儡陨,讓它們來繼承呢,在抽象類中將prepareRecipe方法固定下來量淌,這樣后面的制作的飲料都要遵循這個流程了骗村,接下來煮水和將飲料倒入杯中實際上也是一樣的,也能在抽象的父類Beverage中實現(xiàn)呀枢,steep(浸泡)和brew(沖泡)實際上也沒多大區(qū)別胚股,所以給個新名字brew,最后加入糖和奶跟加入檸檬和奶裙秋,也是類似的琅拌,也給個新名字addCondiments缨伊。改造完之后類圖如下:

image.png

Vander仔細(xì)琢磨了Panda大師的做法,發(fā)現(xiàn)Panda大師實際上先做了泛化(將實現(xiàn)泛化成抽象通用)进宝,接著再將一些具體的步驟交給子類來完成刻坊。Vander想了想其實prepareRecipe完全可以定義成final。這樣就把這個傳統(tǒng)的做法定義下來党晋,不允許子類擅自修改流程紧唱。
Panda大師一看,不錯隶校,你終于學(xué)到精髓了漏益,實際上我們剛剛的做法就是用了模板方法模式。Panda來總結(jié)一下模板方法模式的好處:
1深胳、抽象父類中定義了算法(也就是飲料制作流程)绰疤,保護了算法。
2舞终、對于子類來說轻庆,抽象父類的存在將代碼的復(fù)用最大化。
3敛劝、算法只存在于一個地方余爆,方便修改。
4夸盟、模板方法提供了一個框架蛾方,可以讓其他飲料插進來,新的飲料只要實現(xiàn)自己的方法就行了上陕。
5桩砰、抽象父類專注于方法本身,而由子類提供完整的實現(xiàn)释簿。
Panda大師指導(dǎo)改造后亚隅,代碼成這樣:
Beverage:

public abstract class Beverage {

    public final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }
    
    public void boilWater() {
        System.out.println("Boiling water");
    }
    
    abstract void brew();
    
    public void pourInCup() {
        System.out.println("Pouring into cup");
    }
    
    abstract void addCondiments();
    
}

coffee:

public class Coffee extends Beverage {

    @Override
    public void brew() {
        System.out.println("Dripping Coffee throught filter");
    }

    @Override
    void addCondiments() {
        System.out.println("Adding sugar and milk");
    }
    
}

Milktea:

public class Milktea extends Beverage {

    public void brew() {
        System.out.println("Steeping the tea");
    }

    public void addCondiments() {
        System.out.println("Adding tea and milk");
    }
    
}

說了那么多,模板方法模式具體是什么呢庶溶?
模板方法模式:在一個方法中定義一個算法的骨架煮纵,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結(jié)構(gòu)的情況下偏螺,重新定義算法中的某些步驟行疏。
實際上這個模式是用來創(chuàng)建一個算法的模板。什么是模板呢砖茸,實際上模板就是一個方法隘擎,更具體地說殴穴,這個方法將算法定義成一組步驟凉夯,其中任何步驟都可以是抽象的货葬,由子類負(fù)責(zé)實現(xiàn)。這可以確保算法的結(jié)構(gòu)保持不變劲够,同時由子類提供部分實現(xiàn)震桶。
下面是模板方法模式常用的套路:

image.png

這里需要說明的是,抽象類中有個鉤子方法征绎,這個鉤子方法是在抽象類中提供了一個簡單的實現(xiàn)蹲姐,子類可以選擇去覆蓋它重新實現(xiàn),也可以使用父類的實現(xiàn)人柿。
有了Panda大師度身定制咖啡和奶茶之后柴墩,添加其他飲料更加方便了,很快就加入了蔬菜汁等飲料凫岖。但是有些客人不喜歡喝帶奶的卡布奇諾江咳,他喜歡喝純咖啡。Vander想了想哥放,能不能讓客戶告訴我要不要加奶和糖歼指,我在制作的時候就可以按照客戶的請求來完成了,實際上這是任何一個奶茶店和咖啡店都需要有的基本功能甥雕。Vander是這么改造的:
飲料父類:

public abstract class Beverage {

    public final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        if(customCondiments()) {
            addCondiments();
        }
    }
    
    public void boilWater() {
        System.out.println("Boiling water");
    }
    
    abstract void brew();
    
    public void pourInCup() {
        System.out.println("Pouring into cup");
    }
    
    abstract void addCondiments();
    
    public boolean customCondiments() {
        return true;
    }
    
}

咖啡類:

public class Coffee extends Beverage {

    @Override
    public void brew() {
        System.out.println("Dripping Coffee throught filter");
    }

    @Override
    void addCondiments() {
        System.out.println("Adding sugar and milk");
    }
    
    public boolean customCondiments() {
        String answer = getUserInput();
        if(answer.toLowerCase().startsWith("y")) {
            return true;
        } else {
            return false;
        }
    }
    
    private String getUserInput() {
        String answer = null;
        System.out.println("would you like some milk and sugar with your coffee (y/n)? ");
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        try {
            answer = bufferedReader.readLine();
        } catch (IOException e) {
            System.err.println("IO error trying to read your answer");
        }
        if(answer == null) {
            return "no";
        }
        return answer;
    }
    
}

奶茶類:

public class MilkTea extends Beverage {

    public void brew() {
        System.out.println("Steeping the tea");
    }

    public void addCondiments() {
        System.out.println("Adding sugar and milk");
    }
    
    public boolean customCondiments() {
        String answer = getUserInput();
        if(answer.toLowerCase().startsWith("y")) {
            return true;
        } else {
            return false;
        }
    }
    
    private String getUserInput() {
        String answer = null;
        System.out.println("would you like some milk and sugar with your tea (y/n)? ");
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        try {
            answer = bufferedReader.readLine();
        } catch (IOException e) {
            System.err.println("IO error trying to read your answer");
        }
        if(answer == null) {
            return "no";
        }
        return answer;
    }
    
}

實現(xiàn)的效果:

image.png

下面再說明一下鉤子算法的目的:\color{red}{(實際上就是為了讓子類有能力為其抽象類作一些決定)}
1踩身、讓子類實現(xiàn)算法中的可選部分
2、能夠讓子類有機會對模板方法中某些即將發(fā)生的步驟作出反應(yīng)(如在上面的咖啡類中社露,完全可以讓父類來詢問是否加入佐料挟阻,而子類再進行加入佐料后的一些附加操作)。
還要注意的是峭弟,在寫模板方法時赁濒,不要將算法的步驟切割得太細(xì)(子類要實現(xiàn)太多操作),也不要步驟太少(會沒有彈性)孟害,所以要看情況折衷拒炎。步驟可選的時候就使用鉤子方法。

模板方法模式跟一個設(shè)計原則很吻合挨务,這個設(shè)計原則就是好萊塢原則——別調(diào)用(打電話給)我們击你,我們會調(diào)用(打電話給)你。好萊塢原則可以防止“依賴腐敗”谎柄,什么是依賴腐敗呢丁侄,當(dāng)高層組件依賴底層組件,而底層組件又依賴高層組件朝巫,而高層組件又依賴邊側(cè)組件鸿摇,邊側(cè)組件還依賴于底層組件的時候依賴腐敗就發(fā)生了,這種情況很難搞清楚系統(tǒng)是如何設(shè)計的劈猿。我們允許底層組件將自己掛鉤到系統(tǒng)上拙吉,但是高層組件會決定什么時候和如何使用這些組件潮孽。換句話說:高層組件對待底層組建的方式就是“別調(diào)用我們,我們會調(diào)用你”筷黔。實際上不只是模板方法模式往史,工廠模式和觀察者模式也采用了好萊塢原則。

好萊塢原則 VS 依賴倒置原則

image.png

先對比一下策略模式和模板方法模式:
策略模式:將算法定義成對象佛舱,其他對象通過委托的方式來讓這些算法用起來椎例。
模板方法模式:定義算法的一個大綱,算法中的個別步驟可以有不同的實現(xiàn)細(xì)節(jié)请祖,會重復(fù)使用的代碼可以放進超類中订歪,好讓所有的子類共享。

模板方法模式 VS 策略模式 VS 工廠方法模式

模式 敘述
模板方法 子類決定如何實現(xiàn)算法中的步驟
策略 封裝可互換的行為肆捕,然后使用委托來決定要采用哪一個行為
工廠方法 由子類決定實例化哪個具體類

下面我們要進入Java中的模板方法陌粹,模板方法并非剛剛舉的例子那樣明顯,有些隱藏得頗深福压,讓我們來細(xì)細(xì)挖掘掏秩。
Java數(shù)組類的設(shè)計者給出了一個排序的算法,但是排序的方法中compareTo方法卻是需要我們自身實現(xiàn)的荆姆,因為設(shè)計者無法知道你想按照什么東西來進行排序蒙幻,你必須告訴這個排序算法你依照什么來進行排序的。
接下來看看Sun的源碼:
Comparable接口:

public interface Comparable<T> {   
       public int compareTo(T o);
}

Arrays的相關(guān)函數(shù):

    public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a);
        else
            ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
    }

    /** To be removed in a future release. */
    private static void legacyMergeSort(Object[] a) {
        Object[] aux = a.clone();
        mergeSort(aux, a, 0, a.length, 0);
    }    private static void mergeSort(Object[] src,
                                  Object[] dest,
                                  int low,
                                  int high,
                                  int off) {
        int length = high - low;

        // Insertion sort on smallest arrays
        if (length < INSERTIONSORT_THRESHOLD) {
            for (int i=low; i<high; i++)
                for (int j=i; j>low &&
                         ((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
                    swap(dest, j, j-1);
            return;
        }

        // Recursively sort halves of dest into src
        int destLow  = low;
        int destHigh = high;
        low  += off;
        high += off;
        int mid = (low + high) >>> 1;
        mergeSort(dest, src, low, mid, -off);
        mergeSort(dest, src, mid, high, -off);

        // If list is already sorted, just copy from src to dest.  This is an
        // optimization that results in faster sorts for nearly ordered lists.
        if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) {
            System.arraycopy(src, low, dest, destLow, length);
            return;
        }

        // Merge sorted halves (now in src) into dest
        for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
            if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0)
                dest[i] = src[p++];
            else
                dest[i] = src[q++];
        }
    }

    /**
     * Swaps x[a] with x[b].
     */
    private static void swap(Object[] x, int a, int b) {
        Object t = x[a];
        x[a] = x[b];
        x[b] = t;
    }

具體的排序細(xì)節(jié)見博客中數(shù)據(jù)結(jié)構(gòu)的MergeSort胆筒,這里關(guān)注的是Arrays的Sort方法會調(diào)用數(shù)組的compareTo來進行比較邮破,比較之后再進行位置的對調(diào),這實際上就是讓子類數(shù)組來實現(xiàn)具體的算法細(xì)節(jié)(即比較)仆救,而父類數(shù)組完成算法的步驟(即排序)抒和。
另外,sort()模板方法實現(xiàn)不使用繼承彤蔽,sort方法被定義成一個靜態(tài)的方法摧莽,在運行時和Comparable組合,如果類不實現(xiàn)Comparable接口的話就會導(dǎo)致sort方法中類型轉(zhuǎn)換失敗顿痪。

下面再看一個模板方法的實例JFrame镊辕,里面的paint方法是一個鉤子方法,默認(rèn)情況下是不做事情的蚁袭,要是你繼承了JFrame并且實現(xiàn)了paint方法征懈,它就會進行相應(yīng)的操作。
MyFrame類:

public class MyFrame extends JFrame {

    public MyFrame(String title) {
        super(title);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        this.setSize(300, 300);
        this.setVisible(true);
    }
    
    public void paint(Graphics graphics) {
        super.paint(graphics);
        String msg = "09-Template-Method-Pattern";
        graphics.drawString(msg, 80, 150);
    }
    
}

Main:

public class Main {

    public static void main(String args[]) {
        MyFrame frame = new MyFrame("Design Pattern");
    }

}

我們查看堆棧信息發(fā)現(xiàn)揩悄,paint方法也相當(dāng)于作為一個步驟被其它算法調(diào)用卖哎。

image.png

模板方法定義了算法的步驟,把這些步驟實現(xiàn)延遲到了子類,它是一種代碼復(fù)用的技巧亏娜。
好萊塢原則告訴我們焕窝,將決策權(quán)放在高層模塊中,以便決定如何以及何時調(diào)用低層模塊照藻。
策略模式和模板方法模式都封裝算法袜啃,一個用組合汗侵,一個用繼承幸缕。工廠方法是模板方法的一種特殊版本。
最后又到了喜聞樂見的總結(jié)部分晰韵,我們又來總結(jié)我們現(xiàn)在現(xiàn)有的設(shè)計模式武器发乔。

面向?qū)ο蠡A(chǔ)

抽象、封裝雪猪、多態(tài)栏尚、繼承

八大設(shè)計原則

設(shè)計原則一:封裝變化
設(shè)計原則二:針對接口編程,不針對實現(xiàn)編程
設(shè)計原則三:多用組合只恨,少用繼承
設(shè)計原則四:為交互對象之間的松耦合設(shè)計而努力
設(shè)計原則五:對擴展開放译仗,對修改關(guān)閉
設(shè)計原則六:依賴抽象,不要依賴于具體的類
設(shè)計原則七:只和你的密友談話
設(shè)計原則八:別找我官觅,我有需要會找你

模式

**模板方法模式:在一個方法中定義一個算法的骨架纵菌,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結(jié)構(gòu)的情況下休涤,重新定義算法中的某些步驟咱圆。 ****

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市功氨,隨后出現(xiàn)的幾起案子序苏,更是在濱河造成了極大的恐慌,老刑警劉巖捷凄,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忱详,死亡現(xiàn)場離奇詭異,居然都是意外死亡跺涤,警方通過查閱死者的電腦和手機踱阿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來钦铁,“玉大人软舌,你說我怎么就攤上這事∨2埽” “怎么了佛点?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我超营,道長鸳玩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任演闭,我火速辦了婚禮不跟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘米碰。我一直安慰自己迅办,他們只是感情好报亩,可當(dāng)我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布堆生。 她就那樣靜靜地躺著泉唁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吴趴。 梳的紋絲不亂的頭發(fā)上漆诽,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天,我揣著相機與錄音锣枝,去河邊找鬼厢拭。 笑死,一個胖子當(dāng)著我的面吹牛撇叁,可吹牛的內(nèi)容都是我干的供鸠。 我是一名探鬼主播,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼税朴,長吁一口氣:“原來是場噩夢啊……” “哼回季!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起正林,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤泡一,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后觅廓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鼻忠,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年杈绸,在試婚紗的時候發(fā)現(xiàn)自己被綠了帖蔓。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡瞳脓,死狀恐怖塑娇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情劫侧,我是刑警寧澤埋酬,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布哨啃,位于F島的核電站,受9級特大地震影響写妥,放射性物質(zhì)發(fā)生泄漏拳球。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一珍特、第九天 我趴在偏房一處隱蔽的房頂上張望祝峻。 院中可真熱鬧,春花似錦扎筒、人聲如沸莱找。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宋距。三九已至轴踱,卻和暖如春症脂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背淫僻。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工诱篷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人雳灵。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓棕所,卻偏偏與公主長得像,于是被迫代替她去往敵國和親悯辙。 傳聞我的和親對象是個殘疾皇子琳省,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,747評論 2 361

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