?作者:zuoxiaolong8810(左瀟龍),轉載請注明出處,特別說明:本博文來自博主原博客,為保證新博客中博文的完整性,特復制到此留存,如需轉載請注明新博客地址即可。
????????????????? 模板方法模式,這是一個在許多優秀的開源項目中LZ見的最多的一個設計模式,也是LZ覺得最為優秀的一個設計模式,所以這一章LZ會盡自己所能的去盡量將這個設計模式解釋清楚。
????????????????? 模板方法模式,一般是為了統一子類的算法實現步驟,所使用的一種手段或者說是方式。它在父類中定義一系列算法的步驟,而將具體的實現都推遲到子類。
????????????????? 最典型的形式就是一個接口,一個抽象父類,父類中會有一系列的抽象方法,而在子類中去一一實現這些方法。
????????????????? 下面LZ給舉一個例子,比如我們有一個接口,里面就一個方法,是用來制造一個HTML頁面,如下。
public interface PageBuilder {String bulidHtml();}
???????????????? 這個接口很簡單,就是直接制造一個Html頁面的內容,假設我們不使用模板方法模式,直接讓各個子類去直接實現這個接口,那么肯定實現的方式千奇百怪,而且步驟也亂七八糟的,這樣實在不利于維護和擴展。所以我們可以使用模板方法模式,將這個過程給制定好,然后把具體的內容填充交給子類就好,這樣這些子類生成的HTML頁面就會非常一致。
???????????????? 基于這個目的,我們定義如下抽象類,去實現這個接口,并且我們定義好步驟。
public abstract class AbstractPageBuilder implements PageBuilder{private StringBuffer stringBuffer = new StringBuffer();public String bulidHtml() {//首先加入doctype,因為都是html頁面,所以我們父類不需要推遲給子類實現,直接在父類實現stringBuffer.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">");//頁面下面就是成對的一個HTML標簽,我們也在父類加入,不需要給子類實現stringBuffer.append("<html xmlns=\"http://www.w3.org/1999/xhtml\">");//下面就應該是head標簽里的內容了,這個我們父類做不了主了,推遲到子類實現,所以我們定義一個抽象方法,讓子類必須實現 appendHead(stringBuffer);//下面是body的內容了,我們父類依然無法做主,仍然推遲到子類實現 appendBody(stringBuffer);//html標簽的關閉stringBuffer.append("</html>");return stringBuffer.toString();}//第一個模板方法protected abstract void appendHead(StringBuffer stringBuffer);//第二個模板方法protected abstract void appendBody(StringBuffer stringBuffer);}
? ? ? ? ? ? ? 上面LZ已經加了注釋,這下我們如果要制作一個html頁面,就直接繼承我們的抽象父類就可以了,而我們的子類只需要實現兩個模板方法,就可以成功完成html頁面的創建,下面LZ給出一個子類,我們隨意制造一個html頁面。
public class MyPageBuilder extends AbstractPageBuilder{@Overrideprotected void appendHead(StringBuffer stringBuffer) {stringBuffer.append("<head><title>你好</title></head>");}@Overrideprotected void appendBody(StringBuffer stringBuffer) {stringBuffer.append("<body><h1>你好,世界!</h1></body>");}public static void main(String[] args) {PageBuilder pageBuilder = new MyPageBuilder();System.out.println(pageBuilder.bulidHtml());}}
? ? ? ? ? ? ? ?我們簡單的加入一個head和body標簽,然后創建測試類運行一下,就會發現,我們按照父類給的標準模板,生成了一個html頁面。
? ? ? ? ? ? ? ?這樣做的方式的好處是,父類可以規范子類的創建過程,便于我們維護,而且子類也更省事,因為像doctype包括html標簽都是一樣的,所以子類不再需要關心這些。當然上述LZ寫的有點粗糙,其實我們可以定義的更仔細一點,比如head標簽里,第一個是title,然后是meta等等。但作為例子,我們還是遵循簡單的原則,主要還是想給各位傳達模板方法模式的思想。
? ? ? ? ? ? ? ?模板方法模式是所有設計模式當中,LZ覺得最無侵入性的模式,因為它的好處實在是太明顯了。模板方法模式并不強制接口的實現類必須繼承,所以不會對子類造成任何影響,而如果子類的實現可以配得上模板類的模板,那么就可以享受模板方法模式帶來的好處。
???????????????通常情況下,模板方法模式用于定義構建某個對象的步驟與順序,或者定義一個算法的骨架。
???????????????我們剛才的示例明顯就是構建一個String對象的過程,在這里要聲明一點,對于模板方法模式,父類提供的構建步驟和順序或者算法骨架,通常是不希望甚至是不允許子類去覆蓋的,所以在某些場景中,可以直接將父類中提供骨架的方法聲明為final類型。
? ? ? ? ? ? ? ?模板方法模式還有一種使用的方式,為了給子類足夠的自由度,可以提供一些方法供子類覆蓋,去實現一些骨架中不是必須但卻可以有自定義實現的步驟。
? ? ? ? ? ? ? ?比如上述的例子當中,我們應該都知道,HTML頁面中有一些標簽是可有可無的。比如meta標簽,link標簽,script標簽等。那么我們可以將剛才的例子細化一下,去看一下上面說的供子類覆蓋的方法是什么。我們將剛才的抽象父類細化成如下形式。
public abstract class AbstractPageBuilder implements PageBuilder{private static final String DEFAULT_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">";private static final String DEFAULT_XMLNS = "http://www.w3.org/1999/xhtml";private StringBuffer stringBuffer = new StringBuffer();public String bulidHtml() {stringBuffer.append(DEFAULT_DOCTYPE);stringBuffer.append("<html xmlns=\"" + DEFAULT_XMLNS + "\">");stringBuffer.append("<head>");appendTitle(stringBuffer);appendMeta(stringBuffer);appendLink(stringBuffer);appendScript(stringBuffer);stringBuffer.append("</head>");appendBody(stringBuffer);stringBuffer.append("</html>");return stringBuffer.toString();}protected void appendMeta(StringBuffer stringBuffer){}protected void appendLink(StringBuffer stringBuffer){}protected void appendScript(StringBuffer stringBuffer){}protected abstract void appendTitle(StringBuffer stringBuffer);protected abstract void appendBody(StringBuffer stringBuffer);}
????????????????可以看到,我們將head標簽的生成過程更加細化了,分成四個方法,title,meta,link和script。但是這四個里面appendTitle是模板方法,子類必須實現,而其它三個則是普通的空方法。
??????????????? 那么上述三個方法,就是留給子類覆蓋的,當然子類可以選擇不覆蓋,那么生成的HTML就沒有meta,link和script這三種標簽,如果想有的話,就可以覆蓋其中任意一個,比如下面這樣。
public class MyPageBuilder extends AbstractPageBuilder{protected void appendMeta(StringBuffer stringBuffer) {stringBuffer.append("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />");}protected void appendTitle(StringBuffer stringBuffer) {stringBuffer.append("<title>你好</title>");}protected void appendBody(StringBuffer stringBuffer) {stringBuffer.append("<body>你好,世界!</body>");}public static void main(String[] args) {PageBuilder pageBuilder = new MyPageBuilder();System.out.println(pageBuilder.bulidHtml());}}
??????????????? 我們覆蓋了appendMeta方法,所以我們就可以在head標簽中生成一個meta標簽。如果各位看過上章的適配器模式,其實這里和缺省適配很像,目的都是一樣的,因為如果把appendMeta也寫成抽象方法,那么子類就必須實現,但是meta標簽又不是必須的,所以子類就有可能把appendMeta,appendLink,appendScript方法全空著了。
????????????????所以為了不強制子類實現不必要的抽象方法,但又不剝奪子類自由選擇的權利,我們在父類提供一個默認的空實現,來讓子類自由選擇是否要覆蓋掉這些方法。
??????????????? 說到模板方法模式,我們JDK當中有一個類與它還有一個不得不說的故事,那就是類加載器。
??????????????? JDK類加載器可以大致分為三類,分別是啟動類加載器,擴展類加載器,以及應用程序加載器。
????????????????這三者加載類的路徑分別為如下:
????????????????啟動類加載器:JAVA_HOME/lib目錄下,以及被-Xbootcalsspath參數設定的路徑,不過啟動類加載器加載的類是有限制的,如果JVM不認識的話,你放在這些目錄下也沒用。
????????????????擴展類加載器:JAVA_HOME/lib/ext目錄下,以及被java.ext.dirs系統變量指定的路徑。
????????????????應用程序類加載器:用戶自己的類路徑(classpath),這個類加載器就是我們經常使用的系統類加載器,并且JDK中的抽象類ClassLoader的默認父類加載器就是它。
??????????????? 在這里為什么說類加載器和模板方法模式有關呢,是因為ClassLoader類就使用了模板模式,去保證類加載過程中的唯一性。LZ先給各位看下這個類當中的模板模式的應用。
public abstract class ClassLoader {//這是一個重載方法public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}//這里就是父類算法的定義protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{Class c = findLoadedClass(name);if (c == null) {try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClass0(name);}} catch (ClassNotFoundException e) {c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}//這里留了一個方法給子類選擇性覆蓋protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);}}
????????????? LZ截取了主要的部分,為了突出這三個方法。在上面LZ加了簡單的注釋,相信經過剛才的介紹,各位應該能看出來這是一個模板方法模式,只是它沒有定義抽象方法,因為findClass這個方法,并不是必須實現的,所以JDK選擇留給程序員們自己選擇是否要覆蓋。
??????????????從代碼上我們可以看出,在ClassLoader中定義的算法順序是。
????????????? 1,首先看是否有已經加載好的類。
????????????? 2,如果父類加載器不為空,則首先從父類類加載器加載。
????????????? 3,如果父類加載器為空,則嘗試從啟動加載器加載。
????????????? 4,如果兩者都失敗,才嘗試從findClass方法加載。
??????????????這是JDK類加載器的雙親委派模型,即先從父類加載器加載,直到繼承體系的頂層,否則才會采用當前的類加載器加載。這樣做的目的剛才已經說了,是為了JVM中類的一致性。????????????????????????????
??????????????如果有讀者第一次接觸這方面的知識,估計會比較迷茫,下面LZ給出一個例子。各位猜測下下面程序的運行結果會是什么?
package com.classloader;public class ClassLoaderTest {public static void main(String[] args) throws Exception {Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("com.classloader.ClassLoaderTest");Object entity = clazz.newInstance();System.out.println(entity instanceof ClassLoaderTest);} }
??????????????相信各位都可以毫無疑問的猜測出來,結果應該是true,這是因為entity是ClassLoaderTest類的一個實例,instanceof關鍵字用來判斷一個實例是否屬于一個特定的類型,所以結果就是true。
??????????????那么各位再來猜猜下面這段代碼的運行結果會是什么?
package com.classloader;import java.io.IOException; import java.io.InputStream;class MyClassLoader extends ClassLoader{public Class<?> loadClass(String name) throws ClassNotFoundException {String fileName = name.substring(name.lastIndexOf(".")+1) + ".class";InputStream is = getClass().getResourceAsStream(fileName);if (is == null) {return super.loadClass(name);}try {byte[] b = new byte[is.available()];is.read(b);return defineClass(name, b, 0, b.length);} catch (IOException e) {throw new ClassNotFoundException();}}}public class ClassLoaderTest {public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {ClassLoader classLoader = new MyClassLoader();Class<?> clazz = classLoader.loadClass("com.classloader.ClassLoaderTest");Object entity = clazz.newInstance();System.out.println(entity instanceof ClassLoaderTest);}}
??????????????對于類加載器比較熟悉的讀者們可能覺得這個結果并不出乎意料,可是或許還是有人會比較意外,為什么結果會是false呢?
????????????? 這是因為如果沒有按照ClassLoader中提供的骨架算法去加載類的話,可能會造成JVM中有兩個一模一樣的類信息,他們是來自一個類文件,但卻不是一個加載器加載的,所以這兩個類不相等。
????????????? 這也是類加載器為何要使用模板模式給我們定義好查找的算法,是為了保證我們加載的每一個類在虛擬機當中都有且僅有一個。
????????????? 不過你可能會想,既然如此,為何不把loadClass方法寫成final類型的,這樣不是更安全嗎?
????????????? 這是因為有的時候我們希望JVM當中每一個類有且僅有一個,但有的時候我們希望有兩個,甚至N個,就比如我們的tomcat,你可以想象下,你每一個項目假設都有com.xxx.xxxx.BaseDao等等,如果這些類都是一個的話,你的tomcat還能同時啟動多個WEB服務嗎?雖說tomcat也是遵循的雙親委派模型,但是從此也可以看出來,我們并不是在所有時候都希望同一個全限定名的類在整個JVM里面只有一個。
????????????? 這里提到類加載器,是為了給模板方法一個現有的現實中的例子,以便于有些看多了自己制造的例子的讀者可以換個口味,如果有機會,LZ會在這個系列完結以后,專門開一個系列來和各位分享學習虛擬機過程中的感悟,本次不再過多介紹類加載器的相關內容。
????????????? 另外,如果多掌握一些類加載器的知識,還是對平時的工作和學習有很大幫助的,各位也可以私下去搜索下相關資料。
??????????????好了,模板方法模式就介紹到這里吧,希望各位都有自己的收獲。
????????????? 謝謝觀看。
??????????????下期預告,裝飾器模式。
?
??????????????????????
?????????????????????
版權聲明
作者:zuoxiaolong(左瀟龍)
出處:博客園左瀟龍的技術博客--http://www.cnblogs.com/zuoxiaolong
您的支持是對博主最大的鼓勵,感謝您的認真閱讀。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。