模板方法模式——看看 JDK 和 Spring 是如何優(yōu)雅復用代碼的
前言
模板,顧名思義,它是一個固定化、標準化的東西。
模板方法模式是一種行為設計模式, 它在超類中定義了一個算法的框架, 允許子類在不修改結構的情況下重寫算法的特定步驟。
場景問題
程序員不愿多扯,上來先干兩行代碼
網(wǎng)上模板方法的場景示例特別多,個人感覺還是《Head First 設計模式》中的例子比較好。
假設我們是一家飲品店的師傅,起碼需要以下兩個手藝

真簡單哈,這么看,步驟大同小異,我的第一反應就是寫個業(yè)務接口,不同的飲品實現(xiàn)其中的方法就行,像這樣

畫完類圖,猛地發(fā)現(xiàn),第一步和第三步?jīng)]什么差別,而且做飲品是個流程式的工作,我希望使用時,直接調(diào)用一個方法,就去執(zhí)行對應的制作步驟。
靈機一動,不用接口了,用一個抽象父類,把步驟方法放在一個大的流程方法 makingDrinks() 中,且第一步和第三步,完全一樣,沒必要在子類實現(xiàn),改進如下

再看下我們的設計,感覺還不錯,現(xiàn)在用同一個 makingDrinks() 方法來處理咖啡和茶的制作,而且我們不希望子類覆蓋這個方法,所以可以申明為 final,不同的制作步驟,我們希望子類來提供,必須在父類申明為抽象方法,而第一步和第三步我們不希望子類重寫,所以我們聲明為非抽象方法
public?abstract?class?Drinks?{
????void?boilWater()?{
????????System.out.println("將水煮沸");
????}
????abstract?void?brew();
????void?pourInCup()?{
????????System.out.println("倒入杯子");
????}
????abstract?void?addCondiments();
????
????public?final?void?makingDrinks()?{
????????//熱水
????????boilWater();
????????//沖泡
????????brew();
????????//倒進杯子
????????pourInCup();
????????//加料
????????addCondiments();
????}
}
接著,我們分別處理咖啡和茶,這兩個類只需要繼承父類,重寫其中的抽象方法即可(實現(xiàn)各自的沖泡和添加調(diào)料)
public?class?Tea?extends?Drinks?{
????@Override
????void?brew()?{
????????System.out.println("沖茶葉");
????}
????@Override
????void?addCondiments()?{
????????System.out.println("加檸檬片");
????}
}
public?class?Coffee?extends?Drinks?{
????@Override
????void?brew()?{
????????System.out.println("沖咖啡粉");
????}
????@Override
????void?addCondiments()?{
????????System.out.println("加奶加糖");
????}
}
現(xiàn)在可以上崗了,試著制作下咖啡和茶吧
public?static?void?main(String[]?args)?{
????Drinks?coffee?=?new?Coffee();
????coffee.makingDrinks();
????System.out.println();
????Drinks?tea?=?new?Tea();
????tea.makingDrinks();
}
好嘞,又學會一個設計模式,這就是模板方法模式,我們的 makingDrinks() 就是模板方法。我們可以看到相同的步驟 boilWater() 和 pourInCup() 只在父類中進行即可,不同的步驟放在子類實現(xiàn)。
認識模板方法
在閻宏博士的《JAVA與模式》一書中開頭是這樣描述模板方法(Template Method)模式的:
模板方法模式是類的行為模式。準備一個抽象類,將部分邏輯以具體方法以及具體構造函數(shù)的形式實現(xiàn),然后聲明一些抽象方法來迫使子類實現(xiàn)剩余的邏輯。不同的子類可以以不同的方式實現(xiàn)這些抽象方法,從而對剩余的邏輯有不同的實現(xiàn)。這就是模板方法模式的用意。
寫代碼的一個很重要的思考點就是“變與不變”,程序中哪些功能是可變的,哪些功能是不變的,我們可以把不變的部分抽象出來,進行公共的實現(xiàn),把變化的部分分離出來,用接口來封裝隔離,或用抽象類約束子類行為。模板方法就很好的體現(xiàn)了這一點。
模板方法定義了一個算法的步驟,并允許子類為一個或多個步驟提供實現(xiàn)。
模板方法模式是所有模式中最為常見的幾個模式之一,是基于繼承的代碼復用的基本技術,我們再看下類圖

模板方法模式就是用來創(chuàng)建一個算法的模板,這個模板就是方法,該方法將算法定義成一組步驟,其中的任意步驟都可能是抽象的,由子類負責實現(xiàn)。這樣可以確保算法的結構保持不變,同時由子類提供部分實現(xiàn)。
再回顧下我們制作咖啡和茶的例子,有些顧客要不希望咖啡加糖或者不希望茶里加檸檬,我們要改造下模板方法,在加相應的調(diào)料之前,問下顧客
public?abstract?class?Drinks?{
????void?boilWater()?{
????????System.out.println("將水煮沸");
????}
????abstract?void?brew();
????void?pourInCup()?{
????????System.out.println("倒入杯子");
????}
????abstract?void?addCondiments();
????public?final?void?makingDrinks()?{
????????boilWater();
????????brew();
????????pourInCup();
????????//如果顧客需要,才加料
????????if?(customerLike())?{
????????????addCondiments();
????????}
????}
????//定義一個空的缺省方法,只返回?true
????boolean?customerLike()?{
????????return?true;
????}
}
如上,我們加了一個邏輯判斷,邏輯判斷的方法時一個只返回 true 的方法,這個方法我們叫做 鉤子方法。
鉤子:在模板方法的父類中,我們可以定義一個方法,它默認不做任何事,子類可以視情況要不要覆蓋它,該方法稱為“鉤子”。
鉤子方法一般是空的或者有默認實現(xiàn)。鉤子的存在,可以讓子類有能力對算法的不同點進行掛鉤。而要不要掛鉤,又由子類去決定。
是不是很有用呢,我們再看下咖啡的制作
public?class?Coffee?extends?Drinks?{
????@Override
????void?brew()?{
????????System.out.println("沖咖啡粉");
????}
????@Override
????void?addCondiments()?{
????????System.out.println("加奶加糖");
????}
??//覆蓋了鉤子,提供了自己的詢問功能,讓用戶輸入是否需要加料
????boolean?customerLike()?{
????????String?answer?=?getUserInput();
????????if?(answer.toLowerCase().startsWith("y"))?{
????????????return?true;
????????}?else?{
????????????return?false;
????????}
????}
????//處理用戶的輸入
????private?String?getUserInput()?{
????????String?answer?=?null;
????????System.out.println("您想要加奶加糖嗎?輸入 YES 或 NO");
????????BufferedReader?reader?=?new?BufferedReader(new?InputStreamReader(System.in));
????????try?{
????????????answer?=?reader.readLine();
????????}?catch?(IOException?e)?{
????????????e.printStackTrace();
????????}
????????if?(answer?==?null)?{
????????????return?"no";
????????}
????????return?answer;
????}
}
接著再去測試下代碼,看看結果吧。

我想你應該知道鉤子的好處了吧,它可以作為條件控制,影響抽象類中的算法流程,當然也可以什么都不做。
模板方法有很多種實現(xiàn),有時看起來可能不是我們所謂的“中規(guī)中矩”的設計。接下來我們看下 JDK 和 Spring 中是怎么使用模板方法的。
JDK 中的模板方法
我們寫代碼經(jīng)常會用到 comparable 比較器來對數(shù)組對象進行排序,我們都會實現(xiàn)它的 compareTo() 方法,之后就可以通過 Collections.sort() 或者 Arrays.sort() 方法進行排序了。
具體的實現(xiàn)類就不寫了(可以去 github:starfish-learning 上看我的代碼),看下使用
@Override
public?int?compareTo(Object?o)?{
????Coffee?coffee?=?(Coffee)?o;
????if(this.price?(coffee.price)){
????????return?-1;
????}else?if(this.price?==?coffee.price){
????????return?0;
????}else{
????????return?1;
????}
}
public?static?void?main(String[]?args)?{
??Coffee[]?coffees?=?{new?Coffee("星冰樂",38),
??????????????????????new?Coffee("拿鐵",32),
??????????????????????new?Coffee("摩卡",35)};
?
??Arrays.sort(coffees);
??for?(Coffee?coffee1?:?coffees)?{
????System.out.println(coffee1);
??}
}

你可能會說,這個看著不像我們常規(guī)的模板方法,是的。我們看下比較器實現(xiàn)的步驟
構建對象數(shù)組 通過 Arrays.sort 方法對數(shù)組排序,傳參為 Comparable接口的實例比較時候會調(diào)用我們的實現(xiàn)類的 compareTo()方法將排好序的數(shù)組設置進原數(shù)組中,排序完成
一臉懵逼,這個實現(xiàn)竟然也是模板方法。
這個模式的重點在于提供了一個固定算法框架,并讓子類實現(xiàn)某些步驟,雖然使用繼承是標準的實現(xiàn)方式,但通過回調(diào)來實現(xiàn),也不能說這就不是模板方法。
其實并發(fā)編程中最常見,也是面試必問的 AQS 就是一個典型的模板方法。
Spring 中的模板方法
Spring 中的設計模式太多了,而且大部分擴展功能都可以看到模板方式模式的影子。
我們看下 IOC 容器初始化時中的模板方法,不管是 XML 還是注解的方式,對于核心容器啟動流程都是一致的。
AbstractApplicationContext 的 refresh 方法實現(xiàn)了 IOC 容器啟動的主要邏輯。
一個 refresh() 方法包含了好多其他步驟方法,像不像我們說的 模板方法,getBeanFactory() 、refreshBeanFactory() 是子類必須實現(xiàn)的抽象方法,postProcessBeanFactory() 是鉤子方法。
public?abstract?class?AbstractApplicationContext?extends?DefaultResourceLoader
??????implements?ConfigurableApplicationContext?{
?@Override
?public?void?refresh()?throws?BeansException,?IllegalStateException?{
??synchronized?(this.startupShutdownMonitor)?{
???prepareRefresh();
???ConfigurableListableBeanFactory?beanFactory?=?obtainFreshBeanFactory();
???prepareBeanFactory(beanFactory);
????????????postProcessBeanFactory(beanFactory);
????????????invokeBeanFactoryPostProcessors(beanFactory);
????????????registerBeanPostProcessors(beanFactory);
????????????initMessageSource();
????????????initApplicationEventMulticaster();
????????????onRefresh();
????????????registerListeners();
????????????finishBeanFactoryInitialization(beanFactory);
????????????finishRefresh();
??}
?}
????//?兩個抽象方法
????@Override
?public?abstract?ConfigurableListableBeanFactory?getBeanFactory()?throws???IllegalStateException;?
????
????protected?abstract?void?refreshBeanFactory()?throws?BeansException,?IllegalStateException;
????
????//鉤子方法
????protected?void?postProcessBeanFactory(ConfigurableListableBeanFactory?beanFactory)?{
?}
?}
打開你的 IDEA,我們會發(fā)現(xiàn)常用的 ClassPathXmlApplicationContext 和 AnnotationConfigApplicationContext 啟動入口,都是它的實現(xiàn)類(子類的子類的子類的...)。
AbstractApplicationContext的一個子類 AbstractRefreshableWebApplicationContext 中有鉤子方法 onRefresh()的實現(xiàn):
public?abstract?class?AbstractRefreshableWebApplicationContext?extends?……?{
????/**
??*?Initialize?the?theme?capability.
??*/
?@Override
?protected?void?onRefresh()?{
??this.themeSource?=?UiApplicationContextUtils.initThemeSource(this);
?}
}
看下大概的類圖:

小總結
優(yōu)點:1、封裝不變的部分,擴展可變的部分。2、提取公共代碼,便于維護。3、行為由父類控制,子類實現(xiàn)。
缺點:每一個不同的實現(xiàn)都需要一個子類來實現(xiàn),導致類的個數(shù)增加,使得系統(tǒng)更加龐大。
使用場景:1、有多個子類共有的方法,且邏輯相同。2、重要的、復雜的方法,可以考慮作為模板方法。
注意事項:為防止惡意操作,一般模板方法都加上 final 關鍵詞。
參考:
《Head First 設計模式》、《研磨設計模式》
https://sourcemaking.com/design_patterns/template_method
完
? ? ? ?
???覺得不錯,點個在看~

