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

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

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

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)為寫得很漂亮,請(qǐng)來了Panda大師一起來鑒賞,Panda大師一看,這個(gè)實(shí)現(xiàn)有太多冗余代碼了,首先奶茶跟咖啡制作流程既然是相似的,為什么不做成一個(gè)Beverage抽象類,讓它們來繼承呢,在抽象類中將prepareRecipe方法固定下來,這樣后面的制作的飲料都要遵循這個(gè)流程了,接下來煮水和將飲料倒入杯中實(shí)際上也是一樣的,也能在抽象的父類Beverage中實(shí)現(xiàn),steep(浸泡)和brew(沖泡)實(shí)際上也沒多大區(qū)別,所以給個(gè)新名字brew,最后加入糖和奶跟加入檸檬和奶,也是類似的,也給個(gè)新名字addCondiments。改造完之后類圖如下:

image.png

Vander仔細(xì)琢磨了Panda大師的做法,發(fā)現(xiàn)Panda大師實(shí)際上先做了泛化(將實(shí)現(xiàn)泛化成抽象通用),接著再將一些具體的步驟交給子類來完成。Vander想了想其實(shí)prepareRecipe完全可以定義成final。這樣就把這個(gè)傳統(tǒng)的做法定義下來,不允許子類擅自修改流程。
Panda大師一看,不錯(cuò),你終于學(xué)到精髓了,實(shí)際上我們剛剛的做法就是用了模板方法模式。Panda來總結(jié)一下模板方法模式的好處:
1、抽象父類中定義了算法(也就是飲料制作流程),保護(hù)了算法。
2、對(duì)于子類來說,抽象父類的存在將代碼的復(fù)用最大化。
3、算法只存在于一個(gè)地方,方便修改。
4、模板方法提供了一個(gè)框架,可以讓其他飲料插進(jìn)來,新的飲料只要實(shí)現(xiàn)自己的方法就行了。
5、抽象父類專注于方法本身,而由子類提供完整的實(shí)現(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");
    }
    
}

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

image.png

這里需要說明的是,抽象類中有個(gè)鉤子方法,這個(gè)鉤子方法是在抽象類中提供了一個(gè)簡(jiǎn)單的實(shí)現(xiàn),子類可以選擇去覆蓋它重新實(shí)現(xiàn),也可以使用父類的實(shí)現(xiàn)。
有了Panda大師度身定制咖啡和奶茶之后,添加其他飲料更加方便了,很快就加入了蔬菜汁等飲料。但是有些客人不喜歡喝帶奶的卡布奇諾,他喜歡喝純咖啡。Vander想了想,能不能讓客戶告訴我要不要加奶和糖,我在制作的時(shí)候就可以按照客戶的請(qǐng)求來完成了,實(shí)際上這是任何一個(gè)奶茶店和咖啡店都需要有的基本功能。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;
    }
    
}

實(shí)現(xiàn)的效果:

image.png

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

模板方法模式跟一個(gè)設(shè)計(jì)原則很吻合,這個(gè)設(shè)計(jì)原則就是好萊塢原則——?jiǎng)e調(diào)用(打電話給)我們,我們會(huì)調(diào)用(打電話給)你。好萊塢原則可以防止“依賴腐敗”,什么是依賴腐敗呢,當(dāng)高層組件依賴底層組件,而底層組件又依賴高層組件,而高層組件又依賴邊側(cè)組件,邊側(cè)組件還依賴于底層組件的時(shí)候依賴腐敗就發(fā)生了,這種情況很難搞清楚系統(tǒng)是如何設(shè)計(jì)的。我們?cè)试S底層組件將自己掛鉤到系統(tǒng)上,但是高層組件會(huì)決定什么時(shí)候和如何使用這些組件。換句話說:高層組件對(duì)待底層組建的方式就是“別調(diào)用我們,我們會(huì)調(diào)用你”。實(shí)際上不只是模板方法模式,工廠模式和觀察者模式也采用了好萊塢原則。

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

image.png

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

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

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

下面我們要進(jìn)入Java中的模板方法,模板方法并非剛剛舉的例子那樣明顯,有些隱藏得頗深,讓我們來細(xì)細(xì)挖掘。
Java數(shù)組類的設(shè)計(jì)者給出了一個(gè)排序的算法,但是排序的方法中compareTo方法卻是需要我們自身實(shí)現(xiàn)的,因?yàn)樵O(shè)計(jì)者無法知道你想按照什么東西來進(jìn)行排序,你必須告訴這個(gè)排序算法你依照什么來進(jìn)行排序的。
接下來看看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方法會(huì)調(diào)用數(shù)組的compareTo來進(jìn)行比較,比較之后再進(jìn)行位置的對(duì)調(diào),這實(shí)際上就是讓子類數(shù)組來實(shí)現(xiàn)具體的算法細(xì)節(jié)(即比較),而父類數(shù)組完成算法的步驟(即排序)。
另外,sort()模板方法實(shí)現(xiàn)不使用繼承,sort方法被定義成一個(gè)靜態(tài)的方法,在運(yùn)行時(shí)和Comparable組合,如果類不實(shí)現(xiàn)Comparable接口的話就會(huì)導(dǎo)致sort方法中類型轉(zhuǎn)換失敗。

下面再看一個(gè)模板方法的實(shí)例JFrame,里面的paint方法是一個(gè)鉤子方法,默認(rèn)情況下是不做事情的,要是你繼承了JFrame并且實(shí)現(xiàn)了paint方法,它就會(huì)進(jìn)行相應(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)于作為一個(gè)步驟被其它算法調(diào)用。

image.png

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

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

抽象、封裝、多態(tài)、繼承

八大設(shè)計(jì)原則

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

模式

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,327評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,996評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,316評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,406評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,128評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,524評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,576評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,759評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,310評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,065評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,249評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,821評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,479評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,909評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,140評(píng)論 1 290
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,984評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,228評(píng)論 2 375

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