設計模式(三):模板方法模式
一、星巴克服務員
1.初級服務員
假如你是一位剛入職的星巴克服務員,負責為客人泡制咖啡和茶。公司規定茶和咖啡的泡制要遵循下面的步驟:
於是你按照步驟單上的要求設計了咖啡(Coffee)和茶(Tea)並進行了制作
*****************************Coffee*****************************
/** * 咖啡 * @author wuqi * @Date 2019/2/12 17:31 */ public class Coffee { /** * 準備咖啡 */ public voidprepareRecipe(){ boilWater(); brewCoffeeGrinds(); pourInCup(); addSugarAndMilk(); } /** * 將水煮沸 */ public void boilWater() { System.out.println("Boiling water"); } /** * 用沸水沖泡咖啡 */ public void brewCoffeeGrinds(){ System.out.println("Dripping Coffee through filter"); } /** * 將咖啡倒進杯子裏 */ public void pourInCup(){ System.out.println("Pouring into cup"); } /** * 加糖和牛奶 */ public void addSugarAndMilk(){ System.out.println("Adding Sugar and Milk"); } }
*****************************Tea*****************************
/** * 茶 * @author wuqi * @Date 2019/2/12 17:36 */ public class Tea { /** * 準備茶 */ public void prepareRecipe(){ boilWater(); steepTeaBag(); pourInCup(); addLemon(); } /** * 將水煮沸 */ public void boilWater() { System.out.println("Boiling water"); } /** * 用沸水浸泡茶葉 */ public void steepTeaBag(){ System.out.println("Steeping the tea"); } /** * 將茶倒進杯子裏 */ public void pourInCup(){ System.out.println("Pouring into cup"); } /** * 加檸檬 */ public void addLemon(){ System.out.println("Adding Lemon"); } }
*****************************制作茶和咖啡*****************************
/** * 沒有使用模板方法模式測試 * @author wuqi * @Date 2019/2/12 17:44 */ public class NoTemplateTest { public static void main(String[] args) { System.out.println("Making coffee..."); Coffee coffee = new Coffee(); coffee.prepareRecipe(); System.out.println(); System.out.println("Making Tea..."); Tea tea = new Tea(); tea.prepareRecipe(); } }
制作結果:
你成功的將咖啡制作了出來,但是一個高級服務員從你身旁經過時看了一眼你的制作代碼,嘲諷的說到:你這樣制作的咖啡和茶有太多的重復代碼,如果以後再讓你制作另一種飲料,制作的步驟應該還是類似的,你還要再把重復的代碼寫一遍,最要命的是你竟然沒有對這些飲料進行抽象。你雖然不高興,但是別人把你設計的代碼存在的問題一一指了出來,你還是比較感激的。你恭敬的說到:你說的對,我會改進一下的。
2.高級服務員
根據之前嘲諷你的高級服務員的指導,看著你之前所寫的代碼,你陷入了沈思:首先,我應該將咖啡和茶抽象出來,這兩種飲料都含有咖啡因,就抽象成CaffeineBeverage吧。然後咖啡和茶有兩個完全相同的方法boilWater和pourIntoCup,將這兩個方法放在抽象類中。這還不夠,對於prepareRecipe()來說,咖啡和茶都是遵循著相同的步驟,而且是定死的步驟,不允許我們修改,只是步驟具體實現的方式不一樣,這些方式是由具體的類(咖啡和茶)決定的,所以我可以將這個方法放在抽象類中作為一個模板(template),並且不允許子類重寫,子類只可以重寫某些步驟具體的實現方式,就是怎麽泡飲料,怎麽加調料,這樣的話我就需要將咖啡的brewCoffeeGrinds()和steepTeaBag()泛化成brew()這個名字,都是沖泡,讓子類具體去決定怎麽個泡法,而對於咖啡的addSugarAndMilk()和茶的addLemon()則可以泛化成addCondiments(),就是加調料,具體加什麽調料也由子類去決定。按照這個思路你畫出了相關的UML類圖
按照這個UML類圖,你寫下了改進後的代碼:
*****************************CaffeineBeverage*****************************
/** * 咖啡因飲料 * @author wuqi * @Date 2019/2/12 17:49 */ public abstract class CaffeineBeverage { /** * 抽象類定義模板,定義準備咖啡因飲料的具體步驟 * 該步驟為final,不允許子類修改 */ public final void prepareRecipe(){ boilWater(); brew(); pourInCup(); addCondiment(); } /** * 將水煮沸 */ public void boilWater() { System.out.println("Boiling water"); } /** * 沖泡 */ public abstract void brew(); /** * 將飲料倒進杯子裏 */ public void pourInCup(){ System.out.println("Pouring into cup"); } /** * 加調料 */ public abstract void addCondiment(); }
*****************************Coffee*****************************
/** * 咖啡 * @author wuqi * @Date 2019/2/12 17:54 */ public class Coffee extends CaffeineBeverage { /** * 用水沖泡咖啡 */ @Override public void brew() { System.out.println("Dripping Coffee through filter"); } /** * 添加糖和牛奶 */ @Override public void addCondiment() { System.out.println("Adding Sugar and Milk"); } }
*****************************Tea****************************
/** * 茶 * @author wuqi * @Date 2019/2/12 17:56 */ public class Tea extends CaffeineBeverage { /** * 用水浸泡茶葉 */ @Override public void brew() { System.out.println("Steeping the tea"); } /** * 添加檸檬 */ @Override public void addCondiment() { System.out.println("Adding Lemon"); } }
*****************************測試制作咖啡和茶****************************
/** * 模板方法測試 * @author wuqi * @Date 2019/2/12 17:58 */ public class TemplateTest { public static void main(String[] args) { System.out.println("Making Coffee..."); Coffee coffee = new Coffee(); coffee.prepareRecipe(); System.out.println(); System.out.println("Making Tea..."); Tea tea = new Tea(); tea.prepareRecipe(); } }
運行結果:
運行改進後的代碼,我們依然成功制作了咖啡和茶。但是改進後的代碼更加簡潔了,復用度更高,而且以後如果再多出一個咖啡因飲料讓我們去制作,我們只需要讓新加的類繼承CaffeineBeverage,並重寫brew()和addCondiments方法即可,再也不用每次都添加很多重復的代碼了,這樣就使我們的系統非常的具有彈性。你非常的滿意,看著改進後的代碼,你突然想到這不就是之前我學習設計模式中的模板方法模式嗎!想不到在這裏用到了。這次,你老板從你身旁經過,看著你制作咖啡的代碼,滿意的點了點頭,說到:不錯,你這個咖啡制作的代碼非常好,你用到了模板方法模式,現在開始,我正是聘用你為高級服務員了。你非常詫異也非常高興的說了聲:謝謝老板!隨後你老板又說到:我們的茶後面不準備再加調料了,正好模板方法的鉤子可以派上用場,你去改進一下吧,也當作你作為高級服務員的最後一道考核。你恭敬的回了句:是的!老板!
3.神奇的鉤子
你努力的回憶著模板方法模式的知識,模板方法模式就是在抽象的基類中定義了一個模板,這個模板中規定了完成該方法所需的具體算法,對應於我們制作飲料的各個步驟,子類需要按照這個步驟去執行該方法,步驟中的一些實現方式可以延遲到子類去決定。而鉤子是穿插在算法當中的一個可選的部分,它有默認的實現,鉤子可以由子類實現並決定是否啟用。你又畫了個UML類圖來表示加上鉤子後的飲料制作方式。
根據這個UML類圖,你寫下了加上鉤子的代碼
*****************************CaffeineBeverage****************************
/** * 咖啡因飲料 * @author wuqi * @Date 2019/2/12 17:49 */ public abstract class CaffeineBeverage { /** * 抽象類定義模板,定義準備咖啡因飲料的具體步驟 * 該步驟為final,不允許子類修改 */ public final void prepareRecipe(){ boilWater(); brew(); pourInCup(); if(needAddCondiments()){ addCondiment(); } } /** * 鉤子 * @return */ protected boolean needAddCondiments(){ return false; } /** * 將水煮沸 */ public void boilWater() { System.out.println("Boiling water"); } /** * 沖泡 */ public abstract void brew(); /** * 將飲料倒進杯子裏 */ public void pourInCup(){ System.out.println("Pouring into cup"); } /** * 加調料 */ public abstract void addCondiment(); }
*****************************Coffee****************************
/** * 咖啡 * @author wuqi * @Date 2019/2/12 17:54 */ public class Coffee extends CaffeineBeverage { /** * 用水沖泡咖啡 */ @Override public void brew() { System.out.println("Dripping Coffee through filter"); } /** * 添加糖和牛奶 */ @Override public void addCondiment() { System.out.println("Adding Sugar and Milk"); } /** * 子類覆蓋鉤子 * @return */ @Override public boolean needAddCondiments() { return true; } }
*****************************Tea****************************
/** * 茶 * @author wuqi * @Date 2019/2/12 17:56 */ public class Tea extends CaffeineBeverage { /** * 用水浸泡茶葉 */ @Override public void brew() { System.out.println("Steeping the tea"); } /** * 添加檸檬 */ @Override public void addCondiment() { System.out.println("Adding Lemon"); } }
*****************************TemplateTest****************************
/** * 模板方法測試 * @author wuqi * @Date 2019/2/12 17:58 */ public class TemplateTest { public static void main(String[] args) { System.out.println("Making Coffee..."); Coffee coffee = new Coffee(); coffee.prepareRecipe(); System.out.println(); System.out.println("Making Tea..."); Tea tea = new Tea(); tea.prepareRecipe(); } }
測試結果:
可以看到現在制作茶已經不會再添加檸檬了,因為模板中鉤子默認的是不加調料的,而Tea中沒有重寫鉤子,所以茶現在是不加調料了。你把改進後的代碼交給老板看,你老板非常滿意的點了點頭。從此,你開心的做起了一名高級服務員。
二、定義模板方法
1.模板方法的概念
在一個方法中定義一個算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結構的情況下,重新定義算法中的某些步驟。
2.UML類圖
3.模板方法模式中存在的OO設計原則
1.好萊塢原則
模板方法模式中用到OO設計原則中的好萊塢原則,別調用(打電話給)我們,我們會(打電話給)調用你。
好萊塢原則可以給我們一種防止“依賴腐敗的方法”。當高層組件依賴低層組件,而低層組件又依賴高層組件,而高層組件又依賴邊側組件,而邊側組件又依賴低層組件時,依賴腐敗就發生了。在這種情況下,沒人能輕易搞懂系統是怎麽設計的。在好萊塢原則下,我們允許低層組件將自己掛鉤到系統上,但是高層組件會決定什麽時候和怎樣使用這些低層組件。換句話說,高層組件對待低層組件的方式是“別調用我們,我們會調用你”。
2.模板方法模式和好萊塢原則
模板方法模式就是在抽象基類中定義了一個模板,規定了子類執行的具體步驟和時序,子類只是決定某些步驟的實現方式。抽象基類是高層,子類是低層,這剛好符合好萊塢原則。
三、模板方法模式的應用場景
模板方法模式適用需要定義一套固定的模板,模板中的算法是不更改的,但是某些具體的實現方式可以由子類決定的場景
四、模板方法模式的優缺點
1.優點
- 模板方法模式通過抽象類,可以將代碼的復用最大化
- 具體的算法只存在於模板中,讓系統的修改變得容易,系統會比較有彈性
2.缺點
需要為每一個基本方法的不同實現提供一個子類,如果父類中可變的基本方法太多,將會導致類的個數增加,系統更加龐大,設計也更加抽象。
設計模式(三):模板方法模式