文章目錄
模板方法模式是類的行為模式。準備一個抽象類別,將部分邏輯以具體方法以及具體建構函式的形式實現,然後聲明一些抽象方法來迫使子類實現剩餘的邏輯。不同的子類可以以不同的方式實現這些抽象方法,從而對剩餘的邏輯有不同的實現。這就是模板方法模式的用意。
模板方法模式的結構
模板方法模式是所有模式中最為常見的幾個模式之一,是基於繼承的代碼複用的基本技術。
模板方法模式需要開發抽象類別和具體子類的設計師之間的協作。一個設計師負責給出一個演算法的輪廓和骨架,另一些設計師則負責給出這個演算法的各個邏輯步驟。代表這些具體邏輯步驟的方法稱做基本方法(primitive method);而將這些基本方法匯總起來的方法叫做模板方法(template method),這個設計模式的名字就是從此而來。
模板方法所代表的行為稱為頂級行為,其邏輯稱為頂級邏輯。模板方法模式的靜態結構圖表如下所示:
這裡涉及到兩個角色:
抽象模板(Abstract Template)角色有如下責任:
■ 定義了一個或多個抽象操作,以便讓子類實現。這些抽象操作叫做基本操作,它們是一個頂級邏輯的組成步驟。
■ 定義並實現了一個模板方法。這個模板方法一般是一個具體方法,它給出了一個頂級邏輯的骨架,而邏輯的組成步驟在相應的抽象操作中,延遲到子類實現。頂級邏輯也有可能調用一些具體方法。
具體模板(Concrete Template)角色又如下責任:
■ 實現父類所定義的一個或多個抽象方法,它們是一個頂級邏輯的組成步驟。
■ 每一個抽象模板角色都可以有任意多個具體模板角色與之對應,而每一個具體模板角色都可以給出這些抽象方法(也就是頂級邏輯的組成步驟)的不同實現,從而使得頂級邏輯的實現各不相同。
原始碼
抽象模板角色類,abstractMethod()、hookMethod()等基本方法是頂級邏輯的組成步驟,這個頂級邏輯由templateMethod()方法代表。
package com.bankht.Template;/** * @author: 特種兵—AK47 * @建立時間:2012-6-28 上午10:30:37 * * @類說明 :抽象模板角色類,abstractMethod()、hookMethod()等基本方法是頂級邏輯的組成步驟, * 這個頂級邏輯由templateMethod()方法代表。 */public abstract class AbstractTemplate {/** * 模板方法 */public void templateMethod() {// 調用基本方法abstractMethod();hookMethod();concreteMethod();}/** * 基本方法的聲明(由子類實現)必須由子類置換 */protected abstract void abstractMethod();/** * 基本方法(空方法)可以由子類置換 */protected void hookMethod() {}/** * 基本方法(已經實現)子類不可以動 */private final void concreteMethod() {// 業務相關的代碼}}
具體模板角色類,實現了父類所聲明的基本方法,abstractMethod()方法所代表的就是強制子類實現的剩餘邏輯,而hookMethod()方法是可選擇實現的邏輯,不是必須實現的。
package com.bankht.Template;/** * @author: 特種兵—AK47 * @建立時間:2012-6-28 上午10:37:45 * * @類說明 :具體模板角色類,實現了父類所聲明的基本方法,// * abstractMethod()方法所代表的就是強制子類實現的剩餘邏輯,// * 而hookMethod()方法是可選擇實現的邏輯,不是必須實現的。 */public class ConcreteTemplate extends AbstractTemplate {// 基本方法的實現@Overridepublic void abstractMethod() {// 業務相關的代碼}// 重寫父類的方法@Overridepublic void hookMethod() {// 業務相關的代碼}}
模板模式的關鍵是:子類可以置換掉父類的可變部分,但是子類卻不可以改變模板方法所代表的頂級邏輯。
每當定義一個新的子類時,不要按照控制流程程的思路去想,而應當按照“責任”的思路去想。換言之,應當考慮哪些操作是必須置換掉的,哪些操作是可以置換掉的,以及哪些操作是不可以置換掉的。使用模板模式可以使這些責任變得清晰。
模板方法模式中的方法
模板方法中的方法可以分為兩大類:模板方法和基本方法。
模板方法
一個模板方法是定義在抽象類別中的,把基本操作方法組合在一起形成一個總演算法或一個總行為的方法。
一個抽象類別可以有任意多個模板方法,而不限於一個。每一個模板方法都可以調用任意多個具體方法。
基本方法
基本方法又可以分為三種:抽象方法(Abstract Method)、具體方法(Concrete Method)和鉤子方法(Hook Method)。
● 抽象方法:一個抽象方法由抽象類別聲明,由具體子類實現。在Java語言裡抽象方法以abstract關鍵字標示。
● 具體方法:一個具體方法由抽象類別聲明並實現,而子類並不實現或置換。
● 鉤子方法:一個鉤子方法由抽象類別聲明並實現,而子類會加以擴充。通常抽象類別給出的實現是一個空實現,作為方法的預設實現。
在上面的例子中,AbstractTemplate是一個抽象類別,它帶有三個方法。其中abstractMethod()是一個抽象方法,它由抽象類別聲明為抽象方法,並由子類實現;hookMethod()是一個鉤子方法,它由抽象類別聲明並提供預設實現,並且由子類置換掉。concreteMethod()是一個具體方法,它由抽象類別聲明並實現。
預設鉤子方法
一個鉤子方法常常由抽象類別給出一個空實現作為此方法的預設實現。這種空的鉤子方法叫做“Do Nothing Hook”。顯然,這種預設鉤子方法在預設適配模式裡面已經見過了,一個預設適配模式講的是一個類為一個介面提供一個預設的空實現,從而使得預設適配類的子類不必像實現介面那樣必須給出所有方法的實現,因為通常一個具體類並不需要所有的方法。
命名規則
命名規則是設計師之間賴以溝通的管道之一,使用恰當的命名規則可以協助不同設計師之間的溝通。
鉤子方法的名字應當以do開始,這是熟悉設計模式的Java開發人員的標準做法。在上面的例子中,鉤子方法hookMethod()應當以do開頭;在HttpServlet類中,也遵從這一命名規則,如doGet()、doPost()等方法。
使用情境
考慮一個計算存款利息的例子。假設系統需要支援兩種存款帳號,即貨幣市場(Money Market)帳號和定期存款(Certificate of Deposite)帳號。這兩種帳號的存款利息是不同的,因此,在計算一個存戶的存款利息額時,必須區分兩種不同的帳號類型。
這個系統的總行為應當是計算出利息,這也就決定了作為一個模板方法模式的頂級邏輯應當是利息計算。由於利息計算涉及到兩個步驟:一個基本方法給出帳號種類,另一個基本方法給出利息百分比。這兩個基本方法構成具體邏輯,因為帳號的類型不同,所以具體邏輯會有所不同。
顯然,系統需要一個抽象角色給出頂級行為的實現,而將兩個作為細節步驟的基本方法留給具體子類實現。由於需要考慮的帳號有兩種:一是貨幣市場帳號,二是定期存款帳號。系統的類結構如所示。
原始碼
抽象模板角色類
package com.bankht.Template;/** * @author: 特種兵—AK47 * @建立時間:2012-6-28 上午11:13:22 * * @類說明 :抽象模板角色類 */public abstract class Account {/** * 模板方法,計算利息數額 * * @return 返回利息數額 */public final String calculateInterest() {double interestRate = doCalculateInterestRate();String accountType = doCalculateAccountType();System.out.println(accountType);double amount = calculateAmount(accountType);return amount + "*" + interestRate+ "=" + (amount * interestRate);}/** * 基本方法留給子類實現 */protected abstract String doCalculateAccountType();/** * 基本方法留給子類實現 */protected abstract double doCalculateInterestRate();/** * 基本方法,已經實現 */private double calculateAmount(String accountType) {/** * 省略相關的商務邏輯 */return 7243.00;}}
具體模板角色類
package com.bankht.Template;/** * @author: 特種兵—AK47 * @建立時間:2012-6-28 上午11:14:13 * * @類說明 :具體模板角色類- 定期帳號 */public class CDAccount extends Account {@Overrideprotected String doCalculateAccountType() {return "CDA";}@Overrideprotected double doCalculateInterestRate() {return 0.06;}}
package com.bankht.Template;/** * @author: 特種兵—AK47 * @建立時間:2012-6-28 上午11:13:49 * * @類說明 :具體模板角色類- 貨幣市場帳號 */public class MoneyMarketAccount extends Account {@Overrideprotected String doCalculateAccountType() {return "MMA";}@Overrideprotected double doCalculateInterestRate() {return 0.045;}}
用戶端類
package com.bankht.Template;/** * @author: 特種兵—AK47 * @建立時間:2012-6-28 上午11:14:30 * * @類說明 :用戶端類 */public class Client {public static void main(String[] args) {Account account = new MoneyMarketAccount();System.out.println("貨幣市場帳號的利息數額為:" + account.calculateInterest());account = new CDAccount();System.out.println("定期帳號的利息數額為:" + account.calculateInterest());}}
運行一下:
MMA貨幣市場帳號的利息數額為:7243.0*0.045=325.935CDA定期帳號的利息數額為:7243.0*0.06=434.58
模板方法模式在Servlet中的應用
使用過Servlet的人都清楚,除了要在web.xml做相應的配置外,還需繼承一個叫HttpServlet的抽象類別。HttpService類提供了一個service()方法,這個方法調用七個do方法中的一個或幾個,完成對用戶端調用的響應。這些do方法需要由HttpServlet的具體子類提供,因此這是典型的模板方法模式。下面是service()方法的原始碼:
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); if (method.equals(METHOD_GET)) { long lastModified = getLastModified(req); if (lastModified == -1) { // servlet doesn't support if-modified-since, no reason // to go through further expensive logic doGet(req, resp); } else { long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE); if (ifModifiedSince < (lastModified / 1000 * 1000)) { // If the servlet mod time is later, call doGet() // Round down to the nearest second for a proper compare // A ifModifiedSince of -1 will always be less maybeSetLastModified(resp, lastModified); doGet(req, resp); } else { resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } } } else if (method.equals(METHOD_HEAD)) { long lastModified = getLastModified(req); maybeSetLastModified(resp, lastModified); doHead(req, resp); } else if (method.equals(METHOD_POST)) { doPost(req, resp); } else if (method.equals(METHOD_PUT)) { doPut(req, resp); } else if (method.equals(METHOD_DELETE)) { doDelete(req, resp); } else if (method.equals(METHOD_OPTIONS)) { doOptions(req,resp); } else if (method.equals(METHOD_TRACE)) { doTrace(req,resp); } else { // // Note that this means NO servlet supports whatever // method was requested, anywhere on this server. // String errMsg = lStrings.getString("http.method_not_implemented"); Object[] errArgs = new Object[1]; errArgs[0] = method; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg); } }
當然,這個service()方法也可以被子類置換掉。
下面給出一個簡單的Servlet例子:
從上面的類圖可以看出,TestServlet類是HttpServlet類的子類,並且置換掉了父類的兩個方法:doGet()和doPost()。
public class TestServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("using the GET method"); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("using the POST method"); }}
從上面的例子可以看出這是一個典型的模板方法模式。
HttpServlet擔任抽象模板角色
模板方法:由service()方法擔任。
基本方法:由doPost()、doGet()等方法擔任。
TestServlet擔任具體模板角色
TestServlet置換掉了父類HttpServlet中七個基本方法中的其中兩個,分別是doGet()和doPost()。