模板方法模式
標簽: 設計模式
初識模板方法模式
定義
定義一個操作中的算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
結構和說明

- AbstractClass:抽象類。用來定義算法骨架和原語操作,在這個類里面,還可以提供算法中通用的實現
- ConcreteClass:具體實現類。用來實現算法骨架中的某些步驟,完成跟特定子類相關的功能。
/**
* 定義模板方法、原語操作等的抽象類
*/
public abstract class AbstractClass {
/**
* 原語操作1,所謂原語操作就是抽象的操作,必須要由子類提供實現
*/
public abstract void doPrimitiveOperation1();
/**
* 原語操作2
*/
public abstract void doPrimitiveOperation2();
/**
* 模板方法,定義算法骨架
*/
public final void templateMethod() {
doPrimitiveOperation1();
doPrimitiveOperation2();
}
}
/**
* 具體實現類,實現原語操作
*/
public class ConcreteClass extends AbstractClass {
public void doPrimitiveOperation1() {
//具體的實現
}
public void doPrimitiveOperation2() {
//具體的實現
}
}
體會模板方法模式
登錄控制
- 先看看普通用戶登錄前台的登錄控制的功能:
- 在前台頁面用戶能輸入用戶名和密碼;提交登錄請求,讓系統去進行登錄控制。
- 后台從數據庫獲取登錄人員的信息判斷從前台傳遞過來的登錄數據和數據庫中已有的數據是否匹配?
- 前台Action,如果匹配就轉向首頁,如果不匹配就返回到登錄頁面,並顯示錯誤提示信息。
- 再來看看工作人員登錄后台的登錄控制功能:
- 前台頁面:用戶能輸入用戶名和密碼;提交登錄請求,讓系統去進行登錄控制
- 后台:從數據庫獲取登錄人員的信息。把從前台傳遞過來的密碼數據,使用相應的加密算法進行加密運算,得到加密后的密碼數據
- 后台:判斷從前台傳遞過來的用戶名和加密后的密碼數據,和數據庫中已有的數據是否匹配
- 前台Action:如果匹配就轉向首頁,如果不匹配就返回到登錄頁面,並顯示錯誤提示信息
不用模式的解決方案
普通用戶登錄
/**
* 描述登錄人員登錄時填寫的信息的數據模型
*/
public class LoginModel {
private String userId,pwd;
//省略getXXX和setXXX方法
}
/**
* 描述用戶信息的數據模型
*/
public class UserModel {
private String uuid,userId,pwd,name;
//省略getXXX和setXXX方法
}
/**
* 普通用戶登錄控制的邏輯處理
*/
public class NormalLogin {
/**
* 判斷登錄數據是否正確,也就是是否能登錄成功
* @param lm 封裝登錄數據的Model
* @return true表示登錄成功,false表示登錄失敗
*/
public boolean login(LoginModel lm) {
//1:從數據庫獲取登錄人員的信息, 就是根據用戶編號去獲取人員的數據
UserModel um = this.findUserByUserId(lm.getUserId());
//2:判斷從前台傳遞過來的登錄數據,和數據庫中已有的數據是否匹配
//先判斷用戶是否存在,如果um為null,說明用戶肯定不存在
if (um != null) {
//如果用戶存在,檢查用戶編號和密碼是否匹配
if (um.getUserId().equals(lm.getUserId())
&& um.getPwd().equals(lm.getPwd())) {
return true;
}
}
return false;
}
/**
* 根據用戶編號獲取用戶的詳細信息
* @param userId 用戶編號
* @return 對應的用戶的詳細信息
*/
private UserModel findUserByUserId(String userId) {
// 這里省略具體的處理,僅做示意,返回一個有默認數據的對象
UserModel um = new UserModel();
um.setUserId(userId);
um.setName("test");
um.setPwd("test");
um.setUuid("User0001");
return um;
}
}
工作人員登錄
/**
* 描述登錄人員登錄時填寫的信息的數據模型
*/
public class LoginModel{
private String workerId,pwd;
}
/**
* 描述工作人員信息的數據模型
*/
public class WorkerModel {
private String uuid,workerId,pwd,name;
}
/**
* 工作人員登錄控制的邏輯處理
*/
public class WorkerLogin {
/**
* 判斷登錄數據是否正確,也就是是否能登錄成功
* @param lm 封裝登錄數據的Model
* @return true表示登錄成功,false表示登錄失敗
*/
public boolean login(LoginModel lm) {
//1:根據工作人員編號去獲取工作人員的數據
WorkerModel wm = this.findWorkerByWorkerId(lm.getWorkerId());
//2:判斷從前台傳遞過來的用戶名和加密后的密碼數據,和數據庫中已有的數據是否匹配
if (wm != null) {
//3:把從前台傳來的密碼數據,使用相應的加密算法進行加密運算
String encryptPwd = this.encryptPwd(lm.getPwd());
//如果工作人員存在,檢查工作人員編號和密碼是否匹配
if (wm.getWorkerId().equals(lm.getWorkerId())
&& wm.getPwd().equals(encryptPwd)) {
return true;
}
}
return false;
}
/**
* 對密碼數據進行加密
* @param pwd 密碼數據
* @return 加密后的密碼數據
*/
private String encryptPwd(String pwd){
//這里對密碼進行加密,省略了
return pwd;
}
/**
* 根據工作人員編號獲取工作人員的詳細信息
* @param workerId 工作人員編號
* @return 對應的工作人員的詳細信息
*/
private WorkerModel findWorkerByWorkerId(String workerId) {
// 這里省略具體的處理,僅做示意,返回一個有默認數據的對象
WorkerModel wm = new WorkerModel();
wm.setWorkerId(workerId);
wm.setName("Worker1");
wm.setPwd("worker1");
wm.setUuid("Worker0001");
return wm;
}
}
不用模式有何問題
看了這里的實現示例,是不是很簡單。但是,仔細看看,總會覺得有點問題,兩種登錄的實現太相似了,現在是完全分開,當作兩個獨立的模塊來實現的,如果今后要擴展功能,比如要添加“控制同一個編號同時只能登錄一次”的功能,那么兩個模塊都需要修改,是很麻煩的。而且,現在的實現中,也有很多相似的地方,顯得很重復。另外,具體的實現和判斷的步驟混合在一起,不利於今后變換功能,比如要變換加密算法等。總之,上面的實現,有兩個很明顯的問題:一是重復或相似代碼太多;二是擴展起來很不方便。那么該怎么解決呢?該如何實現才能讓系統既靈活又能簡潔的實現需求功能呢?
使用模式的解決方案的類圖

/**
* 封裝進行登錄控制所需要的數據
*/
public class LoginModel {
/**
* 登錄人員的編號,通用的,可能是用戶編號,也可能是工作人員編號
*/
private String loginId;
/**
* 登錄的密碼
*/
private String pwd;
}
/**
* 登錄控制的模板
*/
public abstract class LoginTemplate {
/**
* 判斷登錄數據是否正確,也就是是否能登錄成功
* @param lm 封裝登錄數據的Model
* @return true表示登錄成功,false表示登錄失敗
*/
public final boolean login(LoginModel lm){
//1:根據登錄人員的編號去獲取相應的數據
LoginModel dbLm = this.findLoginUser(lm.getLoginId());
if(dbLm!=null){
//2:對密碼進行加密
String encryptPwd = this.encryptPwd(lm.getPwd());
//把加密后的密碼設置回到登錄數據模型里面
lm.setPwd(encryptPwd);
//3:判斷是否匹配
return this.match(lm, dbLm);
}
return false;
}
/**
* 根據登錄編號來查找和獲取存儲中相應的數據
* @param loginId 登錄編號
* @return 登錄編號在存儲中相對應的數據
*/
public abstract LoginModel findLoginUser(String loginId);
/**
* 對密碼數據進行加密
* @param pwd 密碼數據
* @return 加密后的密碼數據
*/
public String encryptPwd(String pwd){
return pwd;
}
/**
* 判斷用戶填寫的登錄數據和存儲中對應的數據是否匹配得上
* @param lm 用戶填寫的登錄數據
* @param dbLm 在存儲中對應的數據
* @return true表示匹配成功,false表示匹配失敗
*/
public boolean match(LoginModel lm,LoginModel dbLm){
if(lm.getLoginId().equals(dbLm.getLoginId())
&& lm.getPwd().equals(dbLm.getPwd())){
return true;
}
return false;
}
}
/**
* 普通用戶登錄控制的邏輯處理
*/
public class NormalLogin extends LoginTemplate{
public LoginModel findLoginUser(String loginId) {
// 這里省略具體的處理,僅做示意,返回一個有默認數據的對象
LoginModel lm = new LoginModel();
lm.setLoginId(loginId);
lm.setPwd("testpwd");
return lm;
}
}
/**
* 工作人員登錄控制的邏輯處理
*/
public class WorkerLogin extends LoginTemplate{
public LoginModel findLoginUser(String loginId) {
// 這里省略具體的處理,僅做示意,返回一個有默認數據的對象
LoginModel lm = new LoginModel();
lm.setLoginId(loginId);
lm.setPwd("workerpwd");
return lm;
}
public String encryptPwd(String pwd){
//覆蓋父類的方法,提供真正的加密實現
//這里對密碼進行加密,比如使用:MD5、3DES等等,省略了
System.out.println("使用MD5進行密碼加密");
return pwd;
}
}
public class Client {
public static void main(String[] args) {
//准備登錄人的信息
LoginModel lm = new LoginModel();
lm.setLoginId("admin");
lm.setPwd("workerpwd");
//准備用來進行判斷的對象
LoginTemplate lt = new WorkerLogin();
LoginTemplate lt2 = new NormalLogin();
//進行登錄測試
boolean flag = lt.login(lm);
System.out.println("可以登錄工作平台="+flag);
boolean flag2 = lt2.login(lm);
System.out.println("可以進行普通人員登錄="+flag2);
}
}
理解模板方法模式
認識模板方法模式
模式的功能
模板方法的功能在於固定算法骨架,而讓具體算法實現可擴展。這在實際應用中非常廣泛,尤其是在設計框架級功能的時候非常有用。框架定義好了算法的步驟,在合適的點讓開發人員進行擴展,實現具體的算法。比如在DAO實現中,設計通用的增刪改查功能,這個后面會給大家示例。模板方法還額外提供了一個好處,就是可以控制子類的擴展。因為在父類里面定義好了算法的步驟,只是在某幾個固定的點才會調用到被子類實現的方法,因此也就只允許在這幾個點來擴展功能,這些個可以被子類覆蓋以擴展功能的方法通常被稱為“鈎子”方法,后面也會給大家示例。
為何不是接口
首先搞清楚抽象類和接口的關系。其次要明了什么時候使用抽象類,那就是通常在“既要約束子類的行為,又要為子類提供公共功能”的時候使用抽象類。按照這個原則來思考模板方法模式的實現,模板方法模式需要固定定義算法的骨架,這個骨架應該只有一份,算是一個公共的行為,但是里面具體的步驟的實現又可能是各不相同的,恰好符合選擇抽象類的原則。把模板實現成為抽象類,為所有的子類提供了公共的功能,就是定義了具體的算法骨架;同時在模板里面把需要由子類擴展的具體步驟的算法定義成為抽象方法,要求子類去實現這些方法,這就約束了子類的行為。因此綜合考慮,用抽象類來實現模板是一個很好的選擇。
變與不變
程序設計的一個很重要的思考點就是“變與不變”,也就是分析程序中哪些功能是可變的,哪些功能是不變的,把不變的部分抽象出來,進行公共的實現,把變化的部分分離出去,用接口來封裝隔離,或用抽象類來約束子類行為。模板方法模式很好的體現了這一點。模板類實現的就是不變的方法和算法的骨架,而需要變化的地方,都通過抽象方法,把具體實現延遲到子類,還通過父類的定義來約束了子類的行為,從而使系統能有更好的復用性和擴展性。
好萊塢法則
什么是好萊塢法則呢?簡單點說,就是“不要找我們,我們會聯系你”。模板方法模式很好的體現了這一點,做為父類的模板會在需要的時候,調用子類相應的方法,也就是由父類來找子類,而不是讓子類來找父類。這是一種反向的控制結構,按照通常的思路,是子類找父類才對,也就是應該是子類來調用父類的方法,因為父類根本就不知道子類,而子類是知道父類的,但是在模板方法模式里面,是父類來找子類,所以是一種反向的控制結構。在Java里面能實現這樣功能的理論依據在哪里呢?理論依據就在於Java的動態綁定采用的是“后期綁定”技術,對於出現子類覆蓋父類方法的情況,在編譯時是看數據類型,運行時看實際的對象類型(new操作符后跟的構造方法是哪個類的),一句話:new誰就調用誰的方法。因此在使用模板方法模式的時候,雖然用的數據類型是模板類型,但是在創建類實例的時候是創建的具體的子類的實例,因此調用的時候,會被動態綁定到子類的方法上去,從而實現反向控制。其實在寫父類的時候,它調用的方法是父類自己的抽象方法,只是在運行的時候被動態綁定到了子類的方法上。
擴展登錄控制
在使用模板方法模式實現過后,如果想要擴展新的功能,有如下幾種情況
- 一種情況是只需要提供新的子類實現就可以了,比如想要切換不同的加密算法,現在是使用的MD5,想要實現使用3DES的加密算法,那就新做一個子類,然后覆蓋實現父類加密的方法,在里面使用3DES來實現即可,已有的實現不需要做任何變化
- 另外一種情況是想要給兩個登錄模塊都擴展同一個功能,這種情況多屬於需要修改模板方法的算法骨架的情況,應該盡量避免,但是萬一前面沒有考慮周全,后來出現了這種情況,怎么辦呢?最好就是重構,也就是考慮修改算法骨架,盡量不要去找其它的替代方式,替代的方式也許能把功能實現了,但是會破壞整個程序的結構。
- 還有一種情況是既需要加入新的功能,也需要新的數據。比如:現在對於普通人員登錄,要實現一個加強版,要求登錄人員除了編號和密碼外,還需要提供注冊時留下的驗證問題和驗證答案,驗證問題和驗證答案是記錄在數據庫中的,不是驗證碼,一般Web開發中登錄使用的驗證碼會放到session中,這里不去討論它。
/**
* 封裝進行登錄控制所需要的數據,在公共數據的基礎上,
* 添加具體模塊需要的數據
*/
public class NormalLoginModel extends LoginModel{
/**
* 密碼驗證問題
*/
private String question;
/**
* 密碼驗證答案
*/
private String answer;
}
/**
* 普通用戶登錄控制加強版的邏輯處理
*/
public class NormalLogin2 extends LoginTemplate{
public LoginModel findLoginUser(String loginId) {
//注意一點:這里使用的是自己需要的數據模型了
NormalLoginModel nlm = new NormalLoginModel();
nlm.setLoginId(loginId);
nlm.setPwd("testpwd");
nlm.setQuestion("testQuestion");
nlm.setAnswer("testAnswer");
return nlm;
}
public boolean match(LoginModel lm,LoginModel dbLm){
//這個方法需要覆蓋,因為現在進行登錄控制的時候,
//需要檢測4個值是否正確,而不僅僅是缺省的2個
//先調用父類實現好的,檢測編號和密碼是否正確
boolean f1 = super.match(lm, dbLm);
if(f1){
//如果編號和密碼正確,繼續檢查問題和答案是否正確
//先把數據轉換成自己需要的數據
NormalLoginModel nlm = (NormalLoginModel)lm;
NormalLoginModel dbNlm = (NormalLoginModel)dbLm;
//檢查問題和答案是否正確
if(dbNlm.getQuestion().equals(nlm.getQuestion())
&& dbNlm.getAnswer().equals(nlm.getAnswer())){
return true;
}
}
return false;
}
}
public class Client {
public static void main(String[] args) {
//准備登錄人的信息
NormalLoginModel nlm = new NormalLoginModel();
nlm.setLoginId("testUser");
nlm.setPwd("testpwd");
nlm.setQuestion("testQuestion");
nlm.setAnswer("testAnswer");
//准備用來進行判斷的對象
LoginTemplate lt3 = new NormalLogin2();
//進行登錄測試
boolean flag3 = lt3.login(nlm);
System.out.println("可以進行普通人員加強版登錄="+flag3);
}
}
模板的寫法
通常在模板里面包含如下操作類型:
- 模板方法:就是定義算法骨架的方法
- 具體的操作:在模板中直接實現某些步驟的方法,通常這些步驟的實現算法是固定的,而且是不怎么變化的,因此就可以當作公共功能實現在模板里面。如果不需提供給子類訪問這些方法的話,還可以是private的。這樣一來,子類的實現就相對簡單些。如果是子類需要訪問,可以把這些方法定義為protected final的,因為通常情況下,這些實現不能夠被子類覆蓋和改變了。
- 具體的AbstractClass操作:在模板中實現某些公共功能,可以提供給子類使用,一般不是具體的算法步驟的實現,只是一些輔助的公共功能。
- 原語操作:就是在模板中定義的抽象操作,通常是模板方法需要調用的操作,是必需的操作,而且在父類中還沒有辦法確定下來如何實現,需要子類來真正實現的方法。
- 鈎子操作:在模板中定義,並提供默認實現的操作。這些方法通常被視為可擴展的點,但不是必須的,子類可以有選擇的覆蓋這些方法,以提供新的實現來擴展功能。比如:模板方法中定義了5步操作,但是根據需要,某一種具體的實現只需要其中的1、2、3這幾個步驟,因此它就只需要覆蓋實現1、2、3這幾個步驟對應的方法。那么4和5步驟對應的方法怎么辦呢,由於有默認實現,那就不用管了。也就是說鈎子操作是可以被擴展的點,但不是必須的。
- Factory Method:在模板方法中,如果需要得到某些對象實例的話,可以考慮通過工廠方法模式來獲取,把具體的構建對象的實現延遲到子類中去。
/**
* 一個較為完整的模版定義示例
*/
public abstract class AbstractTemplate {
/**
* 模板方法,定義算法骨架
*/
public final void templateMethod(){
//第一步
this.operation1();
//第二步
this.operation2();
//第三步
this.doPrimitiveOperation1();
//第四步
this.doPrimitiveOperation2();
//第五步
this.hookOperation1();
}
/**
* 具體操作1,算法中的步驟,固定實現,而且子類不需要訪問
*/
private void operation1(){
//在這里具體的實現
}
/**
* 具體操作2,算法中的步驟,固定實現,子類可能需要訪問,
* 當然也可以定義成public的,不可以被覆蓋,因此是final的
*/
protected final void operation2(){
//在這里具體的實現
}
/**
* 具體的AbstractClass操作,子類的公共功能,
* 但通常不是具體的算法步驟
*/
protected void commonOperation(){
//在這里具體的實現
}
/**
* 原語操作1,算法中的必要步驟,父類無法確定如何真正實現,需要子類來實現
*/
protected abstract void doPrimitiveOperation1();
/**
* 原語操作2,算法中的必要步驟,父類無法確定如何真正實現,需要子類來實現
*/
protected abstract void doPrimitiveOperation2();
/**
* 鈎子操作,算法中的步驟,不一定需要,提供缺省實現
* 由子類選擇並具體實現
*/
protected void hookOperation1(){
//在這里提供缺省的實現
}
/**
* 工廠方法,創建某個對象,這里用Object代替了,在算法實現中可能需要
* @return 創建的某個算法實現需要的對象
*/
protected abstract Object createOneObject();
}
Java回調與模板方法模式
模板方法模式的一個目的,就在於讓其它類來擴展或具體實現在模板中固定的算法骨架中的某些算法步驟。在標准的模板方法模式實現中,主要是使用繼承的方式,來讓父類在運行期間可以調用到子類的方法。其實在Java開發中,還有另外一個方法可以實現同樣的功能或是效果,那就是——Java回調技術,通過回調在接口中定義的方法,調用到具體的實現類中的方法,其本質同樣是利用Java的動態綁定技術,在這種實現中,可以不把實現類寫成單獨的類,而是使用匿名內部類來實現回調方法。
/**
* 封裝進行登錄控制所需要的數據
*/
public class LoginModel {
/**
* 登錄人員的編號,通用的,可能是用戶編號,也可能是工作人員編號
*/
private String loginId;
/**
* 登錄的密碼
*/
private String pwd;
}
/**
* 登錄控制的模板方法需要的回調接口,
* 需要盡可能的把所有需要的接口方法都定義出來,
* 或者說是所有可以被擴展的方法都需要被定義出來
*/
public interface LoginCallback {
/**
* 根據登錄編號來查找和獲取存儲中相應的數據
* @param loginId 登錄編號
* @return 登錄編號在存儲中相對應的數據
*/
public LoginModel findLoginUser(String loginId);
/**
* 對密碼數據進行加密
* @param pwd 密碼數據
* @param template LoginTemplate對象,通過它來調用在
* LoginTemplate中定義的公共方法或缺省實現
* @return 加密后的密碼數據
*/
public String encryptPwd(String pwd, LoginTemplate template);
/**
* 判斷用戶填寫的登錄數據和存儲中對應的數據是否匹配得上
* @param lm 用戶填寫的登錄數據
* @param dbLm 在存儲中對應的數據
* @param template LoginTemplate對象,通過它來調用在
* LoginTemplate中定義的公共方法或缺省實現
* @return true表示匹配成功,false表示匹配失敗
*/
public boolean match(LoginModel lm, LoginModel dbLm, LoginTemplate template);
}
/**
* 登錄控制的模板
*/
public class LoginTemplate {
/**
* 判斷登錄數據是否正確,也就是是否能登錄成功
* @param lm 封裝登錄數據的Model
* @param callback LoginCallback對象
* @return true表示登錄成功,false表示登錄失敗
*/
public final boolean login(LoginModel lm,LoginCallback callback){
//1:根據登錄人員的編號去獲取相應的數據
LoginModel dbLm = callback.findLoginUser(lm.getLoginId());
if(dbLm!=null){
//2:對密碼進行加密
String encryptPwd = callback.encryptPwd(lm.getPwd(),this);
//把加密后的密碼設置回到登錄數據模型里面
lm.setPwd(encryptPwd);
//3:判斷是否匹配
return callback.match(lm, dbLm,this);
}
return false;
}
/**
* 對密碼數據進行加密
* @param pwd 密碼數據
* @return 加密后的密碼數據
*/
public String encryptPwd(String pwd){
return pwd;
}
/**
* 判斷用戶填寫的登錄數據和存儲中對應的數據是否匹配得上
* @param lm 用戶填寫的登錄數據
* @param dbLm 在存儲中對應的數據
* @return true表示匹配成功,false表示匹配失敗
*/
public boolean match(LoginModel lm,LoginModel dbLm){
if(lm.getLoginId().equals(dbLm.getLoginId())
&& lm.getPwd().equals(dbLm.getPwd())){
return true;
}
return false;
}
}
public class Client {
public static void main(String[] args) {
//准備登錄人的信息
LoginModel lm = new LoginModel();
lm.setLoginId("admin");
lm.setPwd("workerpwd");
//准備用來進行判斷的對象
LoginTemplate lt = new LoginTemplate();
//進行登錄測試,先測試普通人員登錄
boolean flag = lt.login(lm,new LoginCallback(){
public String encryptPwd(String pwd, LoginTemplate template) {
//自己不需要,直接轉調模板中的默認實現
return template.encryptPwd(pwd);
}
public LoginModel findLoginUser(String loginId) {
// 這里省略具體的處理,僅做示意,返回一個有默認數據的對象
LoginModel lm = new LoginModel();
lm.setLoginId(loginId);
lm.setPwd("testpwd");
return lm;
}
public boolean match(LoginModel lm, LoginModel dbLm,
LoginTemplate template) {
//自己不需要覆蓋,直接轉調模板中的默認實現
return template.match(lm, dbLm);
}
});
System.out.println("可以進行普通人員登錄="+flag);
//測試工作人員登錄
boolean flag2 = lt.login(lm,new LoginCallback(){
public String encryptPwd(String pwd, LoginTemplate template) {
//覆蓋父類的方法,提供真正的加密實現
//這里對密碼進行加密,比如使用:MD5、3DES等等,省略了
System.out.println("使用MD5進行密碼加密");
return pwd;
}
public LoginModel findLoginUser(String loginId) {
// 這里省略具體的處理,僅做示意,返回一個有默認數據的對象
LoginModel lm = new LoginModel();
lm.setLoginId(loginId);
lm.setPwd("workerpwd");
return lm;
}
public boolean match(LoginModel lm, LoginModel dbLm,
LoginTemplate template) {
//自己不需要覆蓋,直接轉調模板中的默認實現
return template.match(lm, dbLm);
}
});
System.out.println("可以登錄工作平台="+flag2);
}
}
兩種實現方式的比較
- 使用繼承的方式,抽象方法和具體實現的關系,是在編譯期間靜態決定的,是類級關系;使用Java回調,這個關系是在運行期間動態決定的,是對象級的關系。
- 相對而言,使用回調機制會更靈活,因為Java是單繼承的,如果使用繼承的方式,對於子類而言,今后就不能繼承其它對象了,而使用回調,是基於接口的。從另一方面說,回調機制是通過委托的方式來組合功能,它的耦合強度要比繼承低一些,這會給我們更多的靈活性。比如某些模板實現的方法,在回調實現的時候可以不調用模板中的方法,而是調用其它實現中的某些功能,也就是說功能不再局限在模板和回調實現上了,可以更靈活組織功能。
- 相對而言,使用繼承方式會更簡單點,因為父類提供了實現的方法,子類如果不想擴展,那就不用管。如果使用回調機制,回調的接口需要把所有可能被擴展的方法都定義進去,這就導致實現的時候,不管你要不要擴展,你都要實現這個方法,哪怕你什么都不做,只是轉調模板中已有的實現,都要寫出來。事實上,在前面講命令模式的時候也提到了Java回調,還通過退化命令模式來實現了Java回調的功能,所以也有這樣的說法:命令模式可以作為模板方法模式的一種替代實現,那就是因為可以使用Java回調來實現模板方法模式。
典型應用:排序
模板方法模式的一個非常典型的應用,就是實現排序的功能。在java.util包中,有一個Collections類,它里面實現了對列表排序的功能,它提供了一個靜態的sort方法,接受一個列表和一個Comparator接口的實例,這個方法
實現的大致步驟是:
- 先把列表轉換成為對象數組
- 通過Arrays的sort方法來對數組進行排序,傳入Comparator接口的實例
- 然后再把排好序的數組的數據設置回到原來的列表對象中去這其中的算法步驟是固定的,也就是算法骨架是固定的了,只是其中具體比較數據大小的步驟,需要由外部來提供,也就是傳入的Comparator接口的實例,就是用來實現數據比較的,在算法內部會通過這個接口來回調具體的實現。
/**
* 用戶數據模型
*/
public class UserModel {
private String userId,name;
private int age;
public UserModel(String userId,String name,int age) {
this.userId = userId;
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "userId="+userId+",name="+name+",age="+age;
}
}
public class Client {
public static void main(String[] args) {
//准備要測試的數據
UserModel um1 = new UserModel("u1","user1",23);
UserModel um2 = new UserModel("u2","user2",22);
UserModel um3 = new UserModel("u3","user3",21);
UserModel um4 = new UserModel("u4","user4",24);
//添加到列表中
List<UserModel> list = new ArrayList<UserModel>();
list.add(um1);
list.add(um2);
list.add(um3);
list.add(um4);
System.out.println("排序前---------------------〉");
printList(list);
//實現比較器,也可以單獨用一個類來實現
Comparator c = new Comparator(){
public int compare(Object obj1, Object obj2) {
//假如實現按照年齡升序排序
UserModel tempUm1 = (UserModel)obj1;
UserModel tempUm2 = (UserModel)obj2;
if(tempUm1.getAge() > tempUm2.getAge()){
return 1;
}else if(tempUm1.getAge() == tempUm2.getAge()){
return 0;
}else if(tempUm1.getAge() < tempUm2.getAge()){
return -1;
}
return 0;
}};
//排序
Collections.sort(list,c);
System.out.println("排序后---------------------〉");
printList(list);
}
private static void printList(List<UserModel> list){
for(UserModel um : list){
System.out.println(um);
}
}
}
排序,到底是模板方法模式,還是策略模式的實例,到底哪個說法更合適?
- 認為是策略模式的實例的理由:
- 首先上面的排序實現,並沒有如同標准的模板方法模式那樣,使用子類來擴展父類,至少從表面上看不太像模板方法模式;
- 其次排序使用的Comparator的實例,可以看成是不同的算法實現,在具體排序時,會選擇使用不同的Comparator實現,就相當於是在切換算法的實現。
- 認為是模板方法模式的實例的理由:
- 首先,模板方法模式的本質是固定算法骨架,雖然使用繼承是標准的實現方式,但是通過回調來實現,也不能說這就不是模板方法模式;
- 其次,從整體程序上看,排序的算法並沒有改變,不過是某些步驟的實現發生了變化,也就是說通過Comparator來切換的是不同的比較大小的實現,相對於整個排序算法而言,它不過是其中的一個步驟而已。
- 總結:排序的實現,實際上組合使用了模板方法模式和策略模式,從整體來看是模板方法模式,但到了局部,比如排序比較算法的實現上,就是使用的策略模式了。
實現通用增刪改查
為了突出主題,以免分散大家的注意力,我們不去使用Spring和Hibernate這樣的流行框架,也不去使用泛型,只用模板方法模式來實現一個簡單的、用JDBC實現的通用增刪改查的功能。先在數據庫中定義一個演示用的表,演示用的是Oracle數據庫,其實你可以用任意的數據庫,只是數據類型要做相應的調整,簡單的數據字典如下:

/**
* 描述用戶的數據模型
*/
public class UserModel {
private String uuid;
private String name;
private int age;
}
/**
* 描述查詢用戶的條件數據的模型
*/
public class UserQueryModel extends UserModel{
/**
* 年齡是一個區間查詢,也就是年齡查詢的條件可以是:
* age >= 條件值1 and age <= 條件值2
* 把UserModel中的age當作條件值1,
* 這里定義的age2當作條件值2
*/
private int age2;
}
----------
/**
* 一個簡單的實現JDBC增刪改查功能的模板
*/
public abstract class JDBCTemplate {
/**
* 定義當前的操作類型是新增
*/
protected final static int TYPE_CREATE = 1;
/**
* 定義當前的操作類型是修改
*/
protected final static int TYPE_UPDATE = 2;
/**
* 定義當前的操作類型是刪除
*/
protected final static int TYPE_DELETE = 3;
/**
* 定義當前的操作類型是按條件查詢
*/
protected final static int TYPE_CONDITION = 4;
/*---------------------模板方法---------------------*/
/**
* 實現新增的功能
* @param obj 需要被新增的數據對象
*/
public final void create(Object obj){
//1:獲取新增的sql
String sql = this.getMainSql(TYPE_CREATE);
//2:調用通用的更新實現
this.executeUpdate(sql, TYPE_CREATE,obj);
}
/**
* 實現修改的功能
* @param obj 需要被修改的數據對象
*/
public final void update(Object obj){
//1:獲取修改的sql
String sql = this.getMainSql(TYPE_UPDATE);
//2:調用通用的更新實現
this.executeUpdate(sql, TYPE_UPDATE,obj);
}
/**
* 實現刪除的功能
* @param obj 需要被刪除的數據對象
*/
public final void delete(Object obj){
//1:獲取刪除的sql
String sql = this.getMainSql(TYPE_DELETE);
//2:調用通用的更新實現
this.executeUpdate(sql, TYPE_DELETE,obj);
}
/**
* 實現按照條件查詢的功能
* @param qm 封裝查詢條件的數據對象
* @return 符合條件的數據對象集合
*/
public final Collection getByCondition(Object qm){
//1:獲取查詢的sql
String sql = this.getMainSql(TYPE_CONDITION);
//2:調用通用的查詢實現
return this.getByCondition(sql, qm);
}
/*---------------------原語操作---------------------*/
/**
* 獲取操作需要的主干sql
* @param type 操作類型
* @return 操作對應的主干sql
*/
protected abstract String getMainSql(int type);
/**
* 為更新操作的sql中的"?"設置值
* @param type 操作類型
* @param pstmt PreparedStatement對象
* @param obj 操作的數據對象
* @throws Exception
*/
protected abstract void setUpdateSqlValue(int type,PreparedStatement pstmt,Object obj) throws Exception;
/**
* 為通用查詢動態的拼接sql的條件部分,基本思路是:
* 只有用戶填寫了相應的條件,那么才在sql中添加對應的條件
* @param sql sql的主干部分
* @param qm 封裝查詢條件的數據模型
* @return 拼接好的sql語句
*/
protected abstract String prepareQuerySql(String sql,Object qm);
/**
* 為通用查詢的sql動態設置條件的值
* @param pstmt 預處理查詢sql的對象
* @param qm 封裝查詢條件的數據模型
* @throws Exception
*/
protected abstract void setQuerySqlValue(PreparedStatement pstmt,Object qm)throws Exception;
/**
* 把查詢返回的結果集轉換成為數據對象
* @param rs 查詢返回的結果集
* @return 查詢返回的結果集轉換成為數據對象
* @throws Exception
*/
protected abstract Object rs2Object(ResultSet rs)throws Exception;
/*---------------------鈎子操作---------------------*/
/**
* 連接數據庫的默認實現,可以被子類覆蓋
* @return 數據庫連接
* @throws Exception
*/
protected Connection getConnection()throws Exception{
Class.forName("oracle.jdbc.driver.OracleDriver");
return DriverManager.getConnection(
"jdbc:oracle:thin:@localhost:1521:orcl",
"test","test");
}
/**
* 執行查詢
* @param sql 查詢的主干sql語句
* @param qm 封裝查詢條件的數據模型
* @return 查詢后的結果對象集合
*/
protected Collection getByCondition(String sql,Object qm){
Collection col = new ArrayList();
Connection conn = null;
try{
//調用鈎子方法
conn = this.getConnection();
//調用原語操作
sql = this.prepareQuerySql(sql, qm);
PreparedStatement pstmt = conn.prepareStatement(sql);
//調用原語操作
this.setQuerySqlValue(pstmt, qm);
ResultSet rs = pstmt.executeQuery();
while(rs.next()){
//調用原語操作
col.add(this.rs2Object(rs));
}
rs.close();
pstmt.close();
}catch(Exception err){
err.printStackTrace();
}finally{
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return col;
}
/**
* 執行更改數據的sql語句,包括增刪改的功能
* @param sql 需要執行的sql語句
* @param callback 回調接口,回調為sql語句賦值的方法
*/
protected void executeUpdate(String sql,int type,Object obj){
Connection conn = null;
try{
//調用鈎子方法
conn = this.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
//調用原語操作
this.setUpdateSqlValue(type,pstmt,obj);
pstmt.executeUpdate();
pstmt.close();
}catch(Exception err){
err.printStackTrace();
}finally{
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
----------
/**
* 具體的實現用戶管理的增刪改查功能
*/
public class UserJDBC extends JDBCTemplate{
protected String getMainSql(int type) {
//根據操作類型,返回相應的主干sql語句
String sql = "";
if(type == TYPE_CREATE){
sql = "insert into tbl_testuser values(?,?,?)";
}else if(type == TYPE_DELETE){
sql = "delete from tbl_testuser where uuid=?";
}else if(type == TYPE_UPDATE){
sql = "update tbl_testuser set name=?,age=? where uuid=?";
}else if(type == TYPE_CONDITION){
sql = "select * from tbl_testuser where 1=1 ";
}
return sql;
}
protected void setUpdateSqlValue(int type, PreparedStatement pstmt,
Object obj) throws Exception{
//設置增、刪、改操作的sql中"?"對應的值
if(type == TYPE_CREATE){
this.setCreateValue(pstmt, (UserModel)obj);
}else if(type == TYPE_DELETE){
this.setDeleteValue(pstmt, (UserModel)obj);
}else if(type == TYPE_UPDATE){
this.setUpdateValue(pstmt, (UserModel)obj);
}
}
protected Object rs2Object(ResultSet rs)throws Exception{
UserModel um = new UserModel();
String uuid = rs.getString("uuid");
String name = rs.getString("name");
int age = rs.getInt("age");
um.setAge(age);
um.setName(name);
um.setUuid(uuid);
return um;
}
protected String prepareQuerySql(String sql,Object qm){
UserQueryModel uqm = (UserQueryModel)qm;
StringBuffer buffer = new StringBuffer();
buffer.append(sql);
//絕對匹配
if(uqm.getUuid()!=null && uqm.getUuid().trim().length()>0){
buffer.append(" and uuid=? ");
}
//模糊匹配
if(uqm.getName()!=null && uqm.getName().trim().length()>0){
buffer.append(" and name like ? ");
}
//區間匹配
if(uqm.getAge() > 0){
buffer.append(" and age >=? ");
}
if(uqm.getAge2() > 0){
buffer.append(" and age <=? ");
}
return buffer.toString();
}
protected void setQuerySqlValue(PreparedStatement pstmt,Object qm)throws Exception{
UserQueryModel uqm = (UserQueryModel)qm;
int count = 1;
if(uqm.getUuid()!=null && uqm.getUuid().trim().length()>0){
pstmt.setString(count, uqm.getUuid());
count++;
}
if(uqm.getName()!=null && uqm.getName().trim().length()>0){
pstmt.setString(count, "%"+uqm.getName()+"%");
count++;
}
if(uqm.getAge() > 0){
pstmt.setInt(count, uqm.getAge());
count++;
}
if(uqm.getAge2() > 0){
pstmt.setInt(count, uqm.getAge2());
count++;
}
}
private void setCreateValue(PreparedStatement pstmt,UserModel um)throws Exception{
pstmt.setString(1, um.getUuid());
pstmt.setString(2, um.getName());
pstmt.setInt(3, um.getAge());
}
private void setUpdateValue(PreparedStatement pstmt,UserModel um)throws Exception{
pstmt.setString(1, um.getName());
pstmt.setInt(2, um.getAge());
pstmt.setString(3, um.getUuid());
}
private void setDeleteValue(PreparedStatement pstmt,UserModel um)throws Exception{
pstmt.setString(1, um.getUuid());
}
}
public class Client {
public static void main(String[] args) {
UserJDBC uj = new UserJDBC();
//先新增幾條
UserModel um1 = new UserModel();
um1.setUuid("u1");
um1.setName("張三");
um1.setAge(22);
uj.create(um1);
UserModel um2 = new UserModel();
um2.setUuid("u2");
um2.setName("李四");
um2.setAge(25);
uj.create(um2);
UserModel um3 = new UserModel();
um3.setUuid("u3");
um3.setName("王五");
um3.setAge(32);
uj.create(um3);
//測試修改
um3.setName("王五被改了");
um3.setAge(35);
uj.update(um3);
//測試查詢
UserQueryModel uqm = new UserQueryModel();
uqm.setAge(25);
uqm.setAge2(36);
Collection<UserModel> col = uj.getByCondition(uqm);
for(UserModel tempUm : col){
System.out.println(tempUm);
}
}
}
模板方法模式的優缺點
實現代碼復用
算法骨架不容易升級
模板方法模式的本質
固定算法骨架
對設計原則的體現
模板方法很好的體現了開閉原則和里氏替換原則。
- 從設計上,先分離變與不變,然后把不變的部分抽取出來,定義到父類里面,比如算法骨架,比如一些公共的、固定的實現等等。這些不變的部分被封閉起來,盡量不去修改它了,要擴展新的功能,那就使用子類來擴展,通過子類來實現可變化的步驟,對於這種新增功能的做法是開放的。
- 能夠實現統一的算法骨架,通過切換不同的具體實現來切換不同的功能,一個根本原因就是里氏替換原則,遵循這個原則,保證所有的子類實現的是同一個算法模板,並能在使用模板的地方,根據需要,切換不同的具體實現。
何時選用模板方法模式
- 需要固定定義算法骨架,實現一個算法的不變的部分,並把可變的行為留給子類來實現的情況。
- 各個子類中具有公共行為,應該抽取出來,集中在一個公共類中去實現,從而避免代碼重復
- 需要控制子類擴展的情況。模板方法模式會在特定的點來調用子類的方法,這樣只允許在這些點進行擴展
