前言 這一章的模板方法模式,個人感覺它是一個簡單,並且實用的設計模式,先說說它的定義: 模板方法模式定義了一個演算法的步驟,並允許子類別為一個或多個步驟提供其實踐方式。讓子類別在不改變演算法架構的情況下,重新定義演算法中的某些步驟。(百度百科) 額, 這段定義呢,如果說我在不瞭解這個設計模式的時候,我看著 ...
前言
這一章的模板方法模式,個人感覺它是一個簡單,並且實用的設計模式,先說說它的定義:
模板方法模式定義了一個演算法的步驟,並允許子類別為一個或多個步驟提供其實踐方式。讓子類別在不改變演算法架構的情況下,重新定義演算法中的某些步驟。(百度百科)
額, 這段定義呢,如果說我在不瞭解這個設計模式的時候,我看著反正是雲里霧裡的,畢竟定義嘛,就是用一堆看不懂的名詞把一個看不懂的名詞描述出來,但是學了這個設計模式,反過來看,又會覺得它的定義很正確。
模板方法模式的關鍵點有3個:
1,有一個由多個步驟構成的方法(模板方法)
2,子類可以自行實現其中的一個或多個步驟(模板方法中的步驟)
3,架構允許的情況下,子類可以重新定義某些步驟
話說,這不就是上面那段話嗎?列成3點以後咋感覺越看越玄了呢?難道這就是傳說中的玄學編程?
列出來的目的是,後面的例子裡面會依次講到這3點,話不多說,代碼在idea裡面已經蓄勢待發!
模板方法模式基本實現
1,故事背景
在實現之前呢,需要有一個歷史背景,不然不知道來龍去脈,容易印象不深刻,headfirst裡面是這樣的一個例子:
在一個店裡面,有2種飲料,它們的沖泡步驟是這樣的:
1,咖啡:把水煮沸,用沸水沖泡咖啡,倒進杯子,加糖和牛奶
2,茶:把水煮沸,用沸水浸泡茶葉,倒進杯子,加檸檬
當然,本著沒有專業的自動化釀造技術的咖啡店不是一個好的科技公司,這段邏輯當然得用代碼來實現了啊,然後就進入大家最喜歡的貼代碼環節:
/** * 咖啡 */ public class Coffee { /** * 準備 */ public void prepare() { boilWater();//把水煮沸 brewCoffeeGrinds();//沖泡咖啡 pourInCup();//倒進杯子 addSugarAndMilk();//添加糖和牛奶 } /** * 把水煮沸 */ private void boilWater() { System.out.println("把水煮沸"); } /** * 沖泡咖啡 */ private void brewCoffeeGrinds() { System.out.println("用沸水沖泡咖啡"); } /** * 倒進杯子 */ private void pourInCup() { System.out.println("倒進杯子"); } /** * 添加糖和牛奶 */ private void addSugarAndMilk() { System.out.println("添加糖和牛奶"); } } /** * 茶 */ public class Tea { /** * 準備 */ public void prepare() { boilWater();//把水煮沸 steepTeaBag();//泡茶 pourInCup();//倒進杯子 addLemon();//加檸檬 } /** * 把水煮沸 */ private void boilWater() { System.out.println("把水煮沸"); } /** * 泡茶 */ private void steepTeaBag() { System.out.println("用沸水浸泡茶葉"); } /** * 倒進杯子 */ private void pourInCup() { System.out.println("倒進杯子"); } /** * 加檸檬 */ private void addLemon() { System.out.println("添加檸檬"); } }
上面貼了咖啡和茶的實現,對外提供的public方法是prepare()方法,其他的內部方法,都是private(不需要的方法不要提供出去,外面的世界鍋太多,它們還小,經不住那麼多的打擊),按道理來說,上面兩段代碼,思路清晰,註釋完整,代碼整潔。
但是,boilWater(),pourInCup()兩個方法,其實內容是一模一樣的,對於一個程式來說,有2段一模一樣的代碼的時候,就應該思考,是不是有什麼地方不對。因為,有2段就表示要改2個同樣的地方,有10段,就要改10個同樣的地方,System.out.println()當然能改啊。
2,邏輯抽象第一版
/** * 咖啡因的飲料(將燒水和倒進杯子兩個方法抽象出來) */ public abstract class CaffeineBeverage { public abstract void prepare();//子類必須要有一個準備飲料的方法 /** * 把水煮沸 */ protected void boilWater() { System.out.println("把水煮沸"); } /** * 倒進杯子 */ protected void pourInCup() { System.out.println("倒進杯子"); } }
咖啡和茶的實現就會變成下麵這樣:
/** * 咖啡 */ public class Coffee extends CaffeineBeverage{ /** * 準備 */ public void prepare() { boilWater();//把水煮沸 brewCoffeeGrinds();//沖泡咖啡 pourInCup();//倒進杯子 addSugarAndMilk();//添加糖和牛奶 } /** * 沖泡咖啡 */ private void brewCoffeeGrinds() { System.out.println("用沸水沖泡咖啡"); } /** * 添加糖和牛奶 */ private void addSugarAndMilk() { System.out.println("添加糖和牛奶"); } } /** * 茶 */ public class Tea extends CaffeineBeverage{ /** * 準備 */ public void prepare() { boilWater();//把水煮沸 steepTeaBag();//泡茶 pourInCup();//倒進杯子 addLemon();//加檸檬 } /** * 泡茶 */ private void steepTeaBag() { System.out.println("用沸水浸泡茶葉"); } /** * 加檸檬 */ private void addLemon() { System.out.println("添加檸檬"); } }
比如在這個例子中,prepare()方法中的步驟還可以總結,抽象。
原來的沖泡步驟:
1,咖啡:把水煮沸,用沸水沖泡咖啡,倒進杯子,加糖和牛奶
2,茶:把水煮沸,用沸水浸泡茶葉,倒進杯子,加檸檬
抽象後:
咖啡/茶:把水煮沸,用沸水 【沖泡咖啡/浸泡茶葉】,倒進杯子,加 【糖和牛奶/檸檬】
第2步和第4步,還可以抽象成,沖泡,加調味品
3,模板方法模式抽象
那麼抽象類的代碼就會變成這樣:
/** * 咖啡因的飲料(模板方法模式) */ public abstract class CaffeineBeverage { /** * 準備(構造成final方法,防止子類重寫演算法) */ final void prepare() { boilWater(); brew(); pourInCup(); addCondiments(); } /** * 沖泡 */ abstract void brew(); /** * 添加調料 */ abstract void addCondiments(); /** * 把水煮沸 */ public void boilWater() { System.out.println("把水煮沸"); } /** * 倒進杯子 */ public void pourInCup() { System.out.println("倒進杯子"); } }
咖啡和茶的實現如下:
/** * 咖啡 */ public class Coffee extends CaffeineBeverage { /** * 沖泡咖啡 */ void brew() { System.out.println("用沸水沖泡咖啡"); } /** * 添加糖和牛奶 */ void addCondiments() { System.out.println("添加糖和牛奶"); } } /** * 茶 */ public class Tea extends CaffeineBeverage { /** * 泡茶 */ void brew() { System.out.println("用沸水浸泡茶葉"); } /** * 加檸檬 */ void addCondiments() { System.out.println("添加檸檬"); } }
測試類:
/** * 測試類 */ public class Test { public static void main(String[] args) { Tea tea = new Tea(); Coffee coffee = new Coffee(); System.out.println("泡茶..."); tea.prepare(); System.out.println("沖咖啡..."); coffee.prepare(); } }
測試結果:
泡茶...
把水煮沸
用沸水浸泡茶葉
倒進杯子
添加檸檬
沖咖啡...
把水煮沸
用沸水沖泡咖啡
倒進杯子
添加糖和牛奶
這個就是一個模板方法模式比較通用的一個實現了,中間就有模板方法模式的2個要點:
(1),有一個由多個步驟構成的方法,這裡就是prepare(),由4個步驟構成的一個模板方法
(2),子類可以自行實現其中的一個或多個步驟,咖啡和茶分別都實現了brew(),addCondiments()
其實到這個地方呢,模板方法模式的一般邏輯就大概講完了,一般來說,實現也就是上面的那個樣子,但是,還是有很多時候,會出現各種各樣的其他實現方式,畢竟抽象這個東西,對於不同的業務,不同的邏輯,那簡直就是多種多樣,只要不違背設計原則,簡單易用,那麼總會有意識無意識的用到模板方法模式。
接下來就介紹幾種常見的操作。
模板方法模式的常見操作
1,空實現
比如說,在把水煮沸前有一個前置步驟,有的飲料需要,但是有的飲料不需要,那麼就可以在模板方法裡面,給它留一個位置,讓子類去選擇性的覆蓋實現
/** * 咖啡因的飲料(模板方法模式,空實現) */ public abstract class CaffeineBeverage { /** * 準備(構造成final方法,防止子類重寫演算法) */ final void prepare() { beforeBoilWater(); boilWater(); brew(); pourInCup(); addCondiments(); } /** * 燒水前置操作 */ protected void beforeBoilWater(){ } //其他方法省略... }
2,預設實現
比如說,加調味品這個步驟其實是可選的,加不加調味品,每種飲料加不加調味品,模板方法可以交給子類自己去控制。
/** * 咖啡因的飲料(模板方法模式,預設實現) */ public abstract class CaffeineBeverage { /** * 準備(構造成final方法,防止子類重寫演算法) */ final void prepare() { boilWater(); brew(); pourInCup(); if(needCondiments()){ addCondiments(); } } /** * 是否需要調味品 */ protected boolean needCondiments(){ return false; } //其他方法省略... }
總結
模板方法模式,其實就是,在抽象類中定義一個操作中的演算法的步驟,而將一些步驟的實現延遲到子類中。
這裡有一個建議是,儘量在抽象的時候,保證僅存在父類的方法去調用子類的方法,而不要同時存在子類的方法去調用父類的方法。
個人覺得,這樣做的原因有幾點:
1,貫徹這個建議後,整個邏輯會顯得很簡單易懂,反正沒有實現的,在子類就能找到實現
2,相互依賴調來調去的後果就是在系統越來越複雜以後,最後就沒人能看懂了
3,防止抽象類的方法變動引起子類的改動,這個其實不算原因,因為一般來說,抽象類是比較穩定的,而且,子類可以調的抽象類方法在修改時肯定要考量子類的,不允許子類調用的方法肯定都處理過了,一般肯定是調不到的(誒,反射你湊過來幹嘛?)
最後的最後,總結一下模板方法模式的優缺點吧
優點:
1,代碼復用便於維護,子類可擴展
2,行為由父類控制,子類只需要關心自己所需要的步驟即可,開發難度低
缺點:
1,每一個不同的實現都需要一個子類來實現,導致類的個數增加,使得系統更加龐大
所以,如果發現子類很多,是不是要想想是不是設計模式用錯了,去隔壁找找其他的設計模式吧