如果你有一個任務,在一個星期內制造10萬悍馬車模型,只考慮最基本的實現,不考慮太多別的問題,你會怎么做?
既然不考慮擴展性,那就好辦了,先按照最一般的經驗設計類圖,如下圖所示
非常簡單的實現,悍馬車有兩個型號,H1和H2。按照要求,只需要悍馬模型,那就只給悍馬模型,先寫個抽象類,然后兩個不同型號的實現類,通過簡單的繼承就可以實現業務要求。悍馬模型的抽象類代碼如下:
//悍馬模型抽象類 public abstract class HummerModel{ //模型發動 protected abstract void start(); //模型能停止 protected abstract void stop(); //模型喇叭會響 protected abstract void alarm(); //引擎會響 protected abstract void engineBoom(); //模型會跑 protected abstract void run(); }
在抽象類中,我們定義了悍馬模型必須具有的特性,能發動、停止、喇叭會響,引擎可以轟鳴,而且還可以停止。但是每個型號的悍馬實現是不同的,H1型號悍馬實現如下:
public class Hummer1Model extends HummerModel { @Override protected void start() { System.out.println("悍馬H1發動......"); } @Override protected void stop() { System.out.println("悍馬H1停止......"); } @Override protected void alarm() { System.out.println("悍馬H1鳴笛......"); } @Override protected void engineBoom() { System.out.println("悍馬H1引擎聲音是這樣的......"); } @Override protected void run() { //先發動汽車 this.start(); //引擎開始轟鳴 this.engineBoom(); //跑的過程中遇到一條狗,於是按喇叭 this.alarm(); //到達目的地停車 this.stop(); } }
注意看run()方法,這是一個匯總方法,一個模型生產成功了,總要拿給客戶檢測,run()方法就是一種檢驗方法,讓它跑起來,通過run()方法,把所有功能都測試到了。
H2型號悍馬代碼如下:
public class Hummer2Model extends HumanModel { @Override protected void start() { System.out.println("悍馬H2發動......"); } @Override protected void stop() { System.out.println("悍馬H2停止......"); } @Override protected void alarm() { System.out.println("悍馬H2鳴笛......"); } @Override protected void engineBoom() { System.out.println("悍馬H2引擎聲音是這樣的......"); } @Override protected void run() { //先發動汽車 this.start(); //引擎開始轟鳴 this.engineBoom(); //跑的過程中遇到一條狗,於是按喇叭 this.alarm(); //到達目的地停車 this.stop(); } }
程序寫到這里,就發現問題了,兩個實現類的run()方法都是完全相同的,那這個run()方法的實現應該出現在抽象類,而不是實現類上,抽象是所有子類的共性封裝。
注意:在軟件開發過程中,如果相同的一段代碼復制過兩次,就需要對設計產生懷疑,架構師要明確說明為什么相同的邏輯要出現兩次或者更多次。
既然發現問題了,那就需要馬上改正,修改后的類圖如下:
在改正后,在HumerModel中加入了一個run()方法,這個方法是一個實現方法,HumerModel修改后源代碼如下:
//悍馬模型抽象類 public abstract class HumanModel{ //模型發動 protected abstract void start(); //模型能停止 protected abstract void stop(); //模型喇叭會響 protected abstract void alarm(); //引擎會響 protected abstract void engineBoom(); //模型會跑 public final void run(){ //先發動汽車 this.start(); //引擎開始轟鳴 this.engineBoom(); //跑的過程中遇到一條狗,於是按喇叭 this.alarm(); //到達目的地停車 this.stop(); } }
在抽象的悍馬模型上已經定義了run()方法的執行規則,先啟動,然后引擎轟鳴,中間還要按一下喇叭,然后停車,他的兩個具體實現類就不需要實現run()方法了。
場景類實現的任務就是把生產出的模型展現給客戶,其源代碼如下:
//場景類 public class Client{ public static void main(String[] args){ //需要H1型號的悍馬 HummerModel h1 = new Hummer1Model(); //h1模型展示 h1.run(); } }
運行結果如下:
悍馬H1發動...
悍馬H1引擎聲音是這樣的...
悍馬H1鳴笛...
悍馬H1停車...
目前客戶只要求看H1型號的悍馬車,沒問題,生產出來,同時可以運行起來給他看看。
看到這個模式,是不是覺得很簡單,自己一直在用,是的,我們經常在用,但是你不知道這是模板模式,那些所謂的高手就可以很牛地說:“用模板方法模式就可以實現”,你還要很崇拜地看着,哇,牛人,模板方法模式是什么?這就是模板方法模式。
4.1.模板方法模式的定義
模板方法模式(Template Method Pattern)是如此簡單,以致讓你感覺到你已經掌握其精髓了。其定義如下:
Define the skeleton of an algorithm in an operation,deferring some steps to subclass.Template Method lets subclass redefine certain steps of an algorithm without changing the algorithm's structure(定義一個操作中的算法的框架,而將一些步驟延遲到子類中,使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟)
模板方法模式的通用類圖如下:
模板方法模式確實非常簡單,僅僅使用了Java的繼承機制,但它是一個應用非常廣泛的模式。其中,AbstractClass叫做抽象模板,它的方法分為兩類:
基本方法:
基本方法也叫作基本操作,是由子類實現的方法,並且在模板方法被調用。
模板方法:
可以是一個或多個,一般是一個具體方法,也就是一個框架,實現對基本方法的調度,完成固定的邏輯。
注意:為了防止惡意操作,一般模板方法都加上final關鍵字,不允許被覆寫。
在類圖中還有一個角色:具體模板。ConcreteClass1和ConcreteClass2屬於具體模板,實現父類所定義的一個或多個抽象方法,也就是父類定義的基本方法在子類中得以實現。
AbstractClass代碼如下:
//抽象模板類 public abstract class AbstractClass{ //基本方法 protected abstract void doSomething(); //基本方法 protected abstract void doAnything(); //模板方法 public final void templateMethod(){ //調用基本方法,完成相關邏輯 this.doSomething(); this.doAnything(); } }
具體模板代碼如下:
public class ConcreteClass1 extends AbstractClass{ //實現基本方法 protected void doAnything(){ //業務邏輯處理 } protected void doSomething(){ //業務邏輯處理 } } public class ConcreteClass2 extends AbstractClass{ //實現基本方法 protected void doAnything(){ //業務邏輯處理 } protected void doSomething(){ //業務邏輯處理 } }
場景類代碼如下:
//場景類 public class Client{ public static void main(String[] args){ AbstractClass class1 = new ConcreteClass1(); AbstractClass class2 = new ConcreteClass2(); //調用模板方法 class1.templateMethod(); class2.templateMethod(); } }
注意:抽象模板中的基本方法盡量設計為protected類型,符合迪米特法則,不需要暴露的屬性或方法盡量不要設置為protected類型。實現類若非必要,盡量不要擴大父類中的訪問權限.
4.2.模板方法模式的應用場景
1.多個子類有共有的方法,並且邏輯基本相同時。
2.重要、復雜的算法,可以把核心算法設計為模板方法,周邊的相關細節功能則又各個子類實現。
3.重構是,模板方法模式是一個經常使用的模式,把相同的代碼抽取到父類中,然后通過鈎子函數("見模板方法模式的擴展")約束其行為
4.3.模板方法模式的優點
封裝不變的部分,擴展可變的部分
把認為不變的部分的算法封裝到父類實現,而可變部分的則可以通過繼承來繼續擴展。在悍馬模型例子中,就非常容易擴展,例如增加一個H3型號的悍馬模型,很容易實現,添加一個子類,實現父類的基本方法就可以了。
提取公共部分代碼,便於維護
在剛才的例子中就是最好的證明,如果我們不抽取到父類中,任由這種散亂的代碼發生,后果就是當維護人員為了修正一個缺陷,需要到處查找類似的代碼。
行為由父類控制,子類
基本方法是由子類實現的,因此子類可以通過擴展的方式增加相應的功能,符合開閉原則。
4.4.模板方法模式的缺點
按照我們的設計習慣,抽象類負責聲明最抽象、最一般的事物屬性和方法,實現類完成具體的事物屬性和方法。但模板方法卻顛倒了,抽象類定義了部分抽象方法,由子類實現,子類執行的結果影響了父類的結果,也就是子類對父類產生了影響,這在復雜的項目中,會帶來代碼閱讀的難度,而且也會讓新手產生不適感。
4.5.模板方法模式的擴展
如果有一天,客戶提出H1型號的悍馬喇叭想讓它響它就響,H2型號的喇叭不要有聲音。
解決方法如下,先畫出類圖,如下圖:
類圖改動似乎很小,在抽象類HummerModel中添加了一個實現方法isAlarm,確定各個型號的悍馬是否需要聲音,由於各個實現類覆寫該方法,同時其他的基本方法由於不需要對外提供訪問,因此也設計為protected類型,其源代碼如下:
//悍馬模型抽象類 public abstract class HumanModel{ //模型發動 protected abstract void start(); //模型能停止 protected abstract void stop(); //模型喇叭會響 protected abstract void alarm(); //引擎會響 protected abstract void engineBoom(); //模型會跑 public final void run(){ //先發動汽車 this.start(); //引擎開始轟鳴 this.engineBoom(); //喇叭想讓它響就響 if(this.isAlarm()){ this.alarm(); } //到達目的地停車 this.stop(); } //鈎子方法,默認喇叭是會響得。 protected boolean isAlarm(){ return true; } }
在抽象類中,isAlarm()是一個實現方法。其作用是模板方法根據其返回值決定是否要響喇叭,子類可以覆寫該返回值,由於H1型號的喇叭是想讓它響就響,不想讓它響就不響,由人控制,其源碼如下:
public class Hummer1Model extends HummerModel { //要響喇叭 private boolean alarmFlag = true; @Override protected void start() { System.out.println("悍馬H1發動......"); } @Override protected void stop() { System.out.println("悍馬H1停止......"); } @Override protected void alarm() { System.out.println("悍馬H1鳴笛......"); } @Override protected void engineBoom() { System.out.println("悍馬H1引擎聲音是這樣的......"); } @Override protected Boolean isAlarm() { return this.alarmFlag; } //要不要響喇叭,是由客戶來決定的 public void setAlarm(boolean isAlarm){ this.alarmFlag = isAlarm; } }
只要調用H1型號的悍馬,默認是有喇叭響的,也可以通過isAlarm(false)就可以實現,H2型號的悍馬是沒有喇叭響的,其代碼如下:
public class Hummer2Model extends HummerModel { @Override protected void start() { System.out.println("悍馬H1發動......"); } @Override protected void stop() { System.out.println("悍馬H1停止......"); } @Override protected void alarm() { System.out.println("悍馬H1鳴笛......"); } @Override protected void engineBoom() { System.out.println("悍馬H1引擎聲音是這樣的......"); } @Override protected Boolean isAlarm() { return false; } }
場景類代碼如下:
//擴展后的場景類 public class Client{ public static void main(String[] args){ System.out.println("-------H1型號悍馬--------"); System.out.println("H1型號的悍馬是否需要喇叭聲響? 0-不需要 1-需要"); String type=(new BufferedReader(new InputStreamReader(System.in))).r HummerH1Model h1 = new HummerH1Model(); if(type.equals("0")){ h1.setAlarm(false); } h1.run(); System.out.println("\n-------H2型號悍馬--------"); HummerH2Model h2 = new HummerH2Model(); h2.run(); } }
運行是需要交互的, 首先, 要求輸入H1型號的悍馬是否有聲音, 如下所示:
-------H1型號悍馬--------
H1型號的悍馬是否需要喇叭聲響? 0-不需要 1-需要
輸入“0”后的運行結果如下所示:
-------H1型號悍馬--------
H1型號的悍馬是否需要喇叭聲響? 0-不需要 1-需要
0
悍馬H1發動...
悍馬H1引擎聲音是這樣的...
悍馬H1停車...
-------H2型號悍馬--------
悍馬H2發動...
悍馬H2引擎聲音是這樣的...
悍馬H2停車...
輸入“1”后的運行結果如下所示:
-------H1型號悍馬--------
H1型號的悍馬是否需要喇叭聲響? 0-不需要 1-需要
1
悍馬H1發動...
悍馬H1引擎聲音是這樣的...悍馬H1鳴笛...
悍馬H1停車...
-------H2型號悍馬--------
悍馬H2發動...
悍馬H2引擎聲音是這樣的...
悍馬H2停車...
H1型號的悍馬是由客戶自己控制是否要響喇叭, 也就是說外界條件改變, 影 響到模板方法的執行。 在我們的抽象類中isAlarm的返回值就是影響了模板方法的執行結 果, 該方法就叫做鈎子方法(Hook Method) 。 有了鈎子方法模板方法模式才算完美, 大家 可以想想, 由子類的一個方法返回值決定公共部分的執行結果, 是不是很有吸引力呀!
模板方法模式就是在模板方法中按照一定的規則和順序調用基本方法, 具體到前面那個例子, 就是run()方法按照規定的順序(先調用start(), 然后再調用engineBoom(), 再調用alarm(), 最后調用stop())調用本類的其他方法, 並且由isAlarm()方法的返回值確定run()中的執行順序變更。