java設計模式--模板模式
16.1 場景問題
16.1.1 登入控制
幾乎所有的應用系統,都需要系統登入控制的功能,有些系統甚至有多個登入控制的功能,比如:普通使用者可以登入前臺,進行相應的業務操作;而工作人員可以登入後臺,進行相應的系統管理或業務處理。
現在有這麼一個基於Web的企業級應用系統,需要實現這兩種登入控制,直接使用不同的登入頁面來區分它們,把基本的功能需求分別描述如下:
先看看普通使用者登入前臺的登入控制的功能:
前臺頁面:使用者能輸入使用者名稱和密碼;提交登入請求,讓系統去進行登入控制 後臺:從資料庫獲取登入人員的資訊 後臺:判斷從前臺傳遞過來的登入資料,和資料庫中已有的資料是否匹配 前臺Action:如果匹配就轉向首頁,如果不匹配就返回到登入頁面,並顯示錯誤提示資訊
再來看看工作人員登入後臺的登入控制功能:
前臺頁面:使用者能輸入使用者名稱和密碼;提交登入請求,讓系統去進行登入控制
後臺:從資料庫獲取登入人員的資訊
後臺:把從前臺傳遞過來的密碼資料,使用相應的加密演算法進行加密運算,得到加密後的密碼資料
後臺:判斷從前臺傳遞過來的使用者名稱和加密後的密碼資料,和資料庫中已有的資料是否匹配
前臺Action:如果匹配就轉向首頁,如果不匹配就返回到登入頁面,並顯示錯誤提示資訊
說明:普通使用者和工作人員在資料庫裡面是儲存在不同表裡面的;當然也是不同的模組來維護普通使用者的資料和工作人員的資料;另外工作人員的密碼是加密存放的。
16.1.2 不用模式的解決方案
由於普通使用者登入和工作人員登入是不同的模組,有不同的頁面,不同的邏輯處理,不同的資料儲存,因此,在實現上完全當成兩個獨立的小模組去完成了。這裡把它們的邏輯處理部分分別實現出來。
(1)先看普通使用者登入的邏輯處理部分,示例程式碼如下:
/**
* 普通使用者登入控制的邏輯處理
*/
public class NormalLogin {
/**
* 判斷登入資料是否正確,也就是是否能登入成功
* @param lm 封裝登入資料的Model
* @return true表示登入成功,false表示登入失敗
*/
public boolean login(LoginModel lm) {
//1:從資料庫獲取登入人員的資訊,就是根據使用者編號去獲取人員的資料
UserModel um = this.findUserByUserId(lm.getUserId());
//2:判斷從前臺傳遞過來的登入資料,和資料庫中已有的資料是否匹配
//先判斷使用者是否存在,如果um為null,說明使用者肯定不存在
//但是不為null,使用者不一定存在,因為資料層可能返回new UserModel();
//因此還需要做進一步的判斷
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;
}
}
對應的LoginModel,示例程式碼如下:
/**
* 描述登入人員登入時填寫的資訊的資料模型
*/
public class LoginModel {
private String userId,pwd;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
}
對應的UserModel,示例程式碼如下:
/**
* 描述使用者資訊的資料模型
*/
public class UserModel {
private String uuid,userId,pwd,name;
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
(2)再看看工作人員登入的邏輯處理部分,示例程式碼如下:
/**
* 工作人員登入控制的邏輯處理
*/
public class WorkerLogin {
/**
* 判斷登入資料是否正確,也就是是否能登入成功
* @param lm 封裝登入資料的Model
* @return true表示登入成功,false表示登入失敗
*/
public boolean login(LoginModel lm) {
//1:根據工作人員編號去獲取工作人員的資料
WorkerModel wm = findWorkerByWorkerId(lm.getWorkerId());
//2:判斷從前臺傳遞過來的使用者名稱和加密後的密碼資料,
//和資料庫中已有的資料是否匹配
//先判斷工作人員是否存在,如果wm為null,說明工作人員肯定不存在
//但是不為null,工作人員不一定存在,
//因為資料層可能返回new WorkerModel();因此還需要做進一步的判斷
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;
}
}
對應的LoginModel,示例程式碼如下:
/**
* 描述登入人員登入時填寫的資訊的資料模型
*/
public class LoginModel{
private String workerId,pwd;
public String getWorkerId() {
return workerId;
}
public void setWorkerId(String workerId) {
this.workerId = workerId;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
}
對應的WorkerModel,示例程式碼如下:
/**
* 描述工作人員資訊的資料模型
*/
public class WorkerModel {
private String uuid,workerId,pwd,name;
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getWorkerId() {
return workerId;
}
public void setWorkerId(String workerId) {
this.workerId = workerId;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
16.1.3 有何問題
看了上面的實現示例,是不是很簡單。但是,仔細看看,總會覺得有點問題,兩種登入的實現太相似了,現在是完全分開,當作兩個獨立的模組來實現的,如果今後要擴充套件功能,比如要新增“控制同一個編號同時只能登入一次”的功能,那麼兩個模組都需要修改,是很麻煩的。而且,現在的實現中,也有很多相似的地方,顯得很重複。另外,具體的實現和判斷的步驟混合在一起,不利於今後變換功能,比如要變換加密演算法等。
總之,上面的實現,有兩個很明顯的問題:一是重複或相似程式碼太多;二是擴充套件起來很不方便。
那麼該怎麼解決呢?該如何實現才能讓系統既靈活又能簡潔的實現需求功能呢?
16.2 解決方案
16.2.1 模板方法模式來解決
用來解決上述問題的一個合理的解決方案就是模板方法模式。那麼什麼是模板方法模式呢?
(1)模板方法模式定義
(2)應用模板方法模式來解決的思路
仔細分析上面的問題,重複或相似程式碼太多、擴充套件不方便,出現這些問題的原因在哪裡?主要就是兩個實現是完全分開、相互獨立的,沒有從整體上進行控制。如果把兩個模組合起來看,就會發現,那些重複或相似的程式碼就應該被抽取出來,做成公共的功能,而不同的登入控制就可以去擴充套件這些公共的功能。這樣一來,擴充套件的時候,如果出現有相同的功能,那就直接擴充套件公共功能就可以了。
使用模板方法模式,就可以很好的來實現上面的思路。分析上面兩個登入控制模組,會發現它們在實現上,有著大致相同的步驟,只是在每步具體的實現上,略微有些不同,因此,可以把這些運算步驟看作是演算法的骨架,把具體的不同的步驟實現,延遲到子類去實現,這樣就可以通過子類來提供不同的功能實現了。
經過分析總結,登入控制大致的邏輯判斷步驟如下:
根據登入人員的編號去獲取相應的資料
獲取對登入人員填寫的密碼資料進行加密後的資料,如果不需要加密,那就是直接返回登入人員填寫的密碼資料
判斷登入人員填寫的資料和從資料庫中獲取的資料是否匹配
在這三個步驟裡面,第一個和第三個步驟是必不可少的,而第二個步驟是可選的。那麼就可以定義一個父類,在裡面定義一個方法來定義這個演算法骨架,這個方法就是模板方法,然後把父類無法確定的實現,延遲到具體的子類來實現就可以了。
通過這樣的方式,如果要修改加密的演算法,那就在模板的子類裡面重新覆蓋實現加密的方法就好了,完全不需要去改變父類的演算法結構,就可以重新定義這些特定的步驟。
16.2.2 模式結構和說明
AbstractClass:
抽象類。用來定義演算法骨架和原語操作,具體的子類通過重定義這些原語操作來實現一個演算法的各個步驟。在這個類裡面,還可以提供演算法中通用的實現。
ConcreteClass:
具體實現類。用來實現演算法骨架中的某些步驟,完成跟特定子類相關的功能
16.2.3 模板方法模式示例程式碼
(1)先來看看AbstractClass的寫法,示例程式碼如下:
/**
* 定義模板方法、原語操作等的抽象類
*/
public abstract class AbstractClass {
/**
* 原語操作1,所謂原語操作就是抽象的操作,必須要由子類提供實現
*/
public abstract void doPrimitiveOperation1();
/**
* 原語操作2
*/
public abstract void doPrimitiveOperation2();
/**
* 模板方法,定義演算法骨架
*/
public final void templateMethod() {
doPrimitiveOperation1();
doPrimitiveOperation2();
}
}
(2)再看看具體實現類的寫法,示例程式碼如下:
/**
* 具體實現類,實現原語操作
*/
public class ConcreteClass extends AbstractClass {
public void doPrimitiveOperation1() {
//具體的實現
}
public void doPrimitiveOperation2() {
//具體的實現
}
}
16.2.4 使用模板方法模式重寫示例
要使用模板方法模式來實現前面的示例,按照模板方法模式的定義和結構,需要定義出一個抽象的父類,在這個父類裡面定義模板方法,這個模板方法應該實現進行登入控制的整體的演算法步驟。當然公共的功能,就放到這個父類去實現,而這個父類無法決定的功能,就延遲到子類去實現。
這樣一來,兩種登入控制就做為這個父類的子類,分別實現自己需要的功能。
(1)為了把原來的兩種登入控制統一起來,首先需要把封裝登入控制所需要的資料模型統一起來,不再區分是使用者編號還是工作人員編號,而是統一稱為登入人員的編號,還有把其它用不上的資料去掉,這樣就直接使用一個數據模型就可以了。當然,如果各個子類實現需要其它的資料,還可以自行擴充套件。示例程式碼如下:
/**
* 封裝進行登入控制所需要的資料
*/
public class LoginModel {
/**
* 登入人員的編號,通用的,可能是使用者編號,也可能是工作人員編號
*/
private String loginId;
/**
* 登入的密碼
*/
private String pwd;
public String getLoginId() {
return loginId;
}
public void setLoginId(String loginId) {
this.loginId = loginId;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
}
(2)接下來定義公共的登入控制演算法骨架,示例程式碼如下:
/**
* 登入控制的模板
*/
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;
}
}
(3)實現新的普通使用者登入控制的邏輯處理,示例程式碼如下:
/**
* 普通使用者登入控制的邏輯處理
*/
public class NormalLogin extends LoginTemplate{
public LoginModel findLoginUser(String loginId) {
// 這裡省略具體的處理,僅做示意,返回一個有預設資料的物件
LoginModel lm = new LoginModel();
lm.setLoginId(loginId);
lm.setPwd("testpwd");
return lm;
}
}
(4)實現新的工作人員登入控制的邏輯處理,示例程式碼如下
/**
* 工作人員登入控制的邏輯處理
*/
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);
}
}
16.3.1 認識模板方法模式
(1)模式的功能
模板方法的功能在於固定演算法骨架,而讓具體演算法實現可擴充套件。
這在實際應用中非常廣泛,尤其是在設計框架級功能的時候非常有用。框架定義好了演算法的步驟,在合適的點讓開發人員進行擴充套件,實現具體的演算法。比如在DAO實現中,設計通用的增刪改查功能,這個後面會給大家示例。
模板方法還額外提供了一個好處,就是可以控制子類的擴充套件。因為在父類裡面定義好了演算法的步驟,只是在某幾個固定的點才會呼叫到被子類實現的方法,因此也就只允許在這幾個點來擴充套件功能,這些個可以被子類覆蓋以擴充套件功能的方法通常被稱為“鉤子”方法,後面也會給大家示例。
(2)為何不是介面
有的朋友可能會問一個問題,不是說在Java中應該儘量面向介面程式設計嗎,為何模板方法的模板是採用的抽象方法呢?
要回答這個問題,要首先搞清楚抽象類和介面的關係:
介面是一種特殊的抽象類,所有介面中的屬性自動是常量,也就是public final static的,而所有介面中的方法必須是抽象的
抽象類,簡單點說是用abstract修飾的類。這裡要特別注意的是抽象類和抽象方法的關係,記住兩句話:抽象類不一定包含抽象方法;有抽象方法的類一定是抽象類
抽象類和介面相比較,最大的特點就在於抽象類裡面是可以有具體的實現方法的,而介面中所有的方法都是沒有具體的實現的。
因此,雖然Java程式設計中倡導大家“面向介面程式設計”,並不是說就不再使用抽象類了,那麼什麼時候使用抽象類呢?
通常在“既要約束子類的行為,又要為子類提供公共功能”的時候使用抽象類。
按照這個原則來思考模板方法模式的實現,模板方法模式需要固定定義演算法的骨架,這個骨架應該只有一份,算是一個公共的行為,但是裡面具體的步驟的實現又可能是各不相同的,恰好符合選擇抽象類的原則。
把模板實現成為抽象類,為所有的子類提供了公共的功能,就是定義了具體的演算法骨架;同時在模板裡面把需要由子類擴充套件的具體步驟的演算法定義成為抽象方法,要求子類去實現這些方法,這就約束了子類的行為。
因此綜合考慮,用抽象類來實現模板是一個很好的選擇。
(3)變與不變
程式設計的一個很重要的思考點就是“變與不變”,也就是分析程式中哪些功能是可變的,哪些功能是不變的,然後把不變的部分抽象出來,進行公共的實現,把變化的部分分離出去,用介面來封裝隔離,或者是用抽象類來約束子類行為。
模板方法模式很好的體現了這一點。模板類實現的就是不變的方法和演算法的骨架,而需要變化的地方,都通過抽象方法,把具體實現延遲到子類去了,而且還通過父類的定義來約束了子類的行為,從而使系統能有更好的複用性和擴充套件性。
(4)好萊塢法則
什麼是好萊塢法則呢?簡單點說,就是“不要找我們,我們會聯絡你”。
模板方法模式很好的體現了這一點,做為父類的模板會在需要的時候,呼叫子類相應的方法,也就是由父類來找子類,而不是讓子類來找父類。
這其實也是一種反向的控制結構,按照通常的思路,是子類找父類才對,也就是應該是子類來呼叫父類的方法,因為父類根本就不知道子類,而子類是知道父類的,但是在模板方法模式裡面,是父類來找子類,所以是一種反向的控制結構。
那麼,在Java裡面能實現這樣功能的理論依據在哪裡呢?
理論依據就在於Java的動態繫結採用的是“後期繫結”技術,對於出現子類覆蓋父類方法的情況,在編譯時是看資料型別,執行時看實際的物件型別(new操作符後跟的構造方法是哪個類的),一句話:new誰就呼叫誰的方法。
因此在使用模板方法模式的時候,雖然用的資料型別是模板型別,但是在建立類例項的時候是建立的具體的子類的例項,因此呼叫的時候,會被動態繫結到子類的方法上去,從而實現反向控制。其實在寫父類的時候,它呼叫的方法是父類自己的抽象方法,只是在執行的時候被動態繫結到了子類的方法上。
(5)擴充套件登入控制
在使用模板方法模式實現過後,如果想要擴充套件新的功能,有如下幾種情況:
一種情況是隻需要提供新的子類實現就可以了,比如想要切換不同的加密演算法,現在是使用的MD5,想要實現使用3DES的加密演算法,那就新做一個子類,然後覆蓋實現父類加密的方法,在裡面使用3DES來實現即可,已有的實現不需要做任何變化。
另外一種情況是想要給兩個登入模組都擴充套件同一個功能,這種情況多屬於需要修改模板方法的演算法骨架的情況,應該儘量避免,但是萬一前面沒有考慮周全,後來出現了這種情況,怎麼辦呢?最好就是重構,也就是考慮修改演算法骨架,儘量不要去找其它的替代方式,替代的方式也許能把功能實現了,但是會破壞整個程式的結構。
還有一種情況是既需要加入新的功能,也需要新的資料。比如:現在對於普通人員登入,要實現一個加強版,要求登入人員除了編號和密碼外,還需要提供註冊時留下的驗證問題和驗證答案,驗證問題和驗證答案是記錄在資料庫中的,不是驗證碼,一般Web開發中登入使用的驗證碼會放到session中,這裡不去討論它。
假如現在就要進行如此的擴充套件,應該怎麼實現呢?由於需要一些其它的資料,那麼就需要擴充套件LoginModel,加入自己需要的資料;同時可能需要覆蓋由父類提供的一些公共的方法,來實現新的功能。
還是看看程式碼示例吧,會比較清楚。
首先呢,需要擴充套件LoginModel,把具體功能需要的資料封裝起來,只是增加父類沒有的資料就可以了,示例程式碼如下:
/**
* 封裝進行登入控制所需要的資料,在公共資料的基礎上,
* 新增具體模組需要的資料
*/
public class NormalLoginModel extends LoginModel{
/**
* 密碼驗證問題
*/
private String question;
/**
* 密碼驗證答案
*/
private String answer;
public String getQuestion() {
return question;
}
public void setQuestion(String question) {
this.question = question;
}
public String getAnswer() {
return answer;
}
public void setAnswer(String answer) {
this.answer = 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);
}
}
16.3.2 模板的寫法
在實現模板的時候,到底哪些方法實現在模板上呢?模板能不能全部實現了,也就是模板不提供抽象方法呢?當然,就算沒有抽象方法,模板一樣可以定義成為抽象類。
通常在模板裡面包含如下操作型別:
模板方法:就是定義演算法骨架的方法 。
具體的操作:在模板中直接實現某些步驟的方法,通常這些步驟的實現演算法是固定的,而且是不怎麼變化的,因此就可以當作公共功能實現在模板裡面。如果不需提供給子類訪問這些方法的話,還可以是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,演算法中的步驟,固定實現,子類可能需要訪問,
* 當然也可以定義成protected的,不可以被覆蓋,因此是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();
}
對於上面示例的模板寫法,其中定義成為protected的方法,可以根據需要進行調整,如果是允許所有的類都可以訪問這些方法,那麼可以把它們定義成為public的,如果只是子類需要訪問這些方法,那就使用protected的,都是正確的寫法。
16.3.3 Java回撥與模板方法模式
模板方法模式的一個目的,就在於讓其它類來擴充套件或具體實現在模板中固定的演算法骨架中的某些演算法步驟。在標準的模板方法模式實現中,主要是使用繼承的方式,來讓父類在執行期間可以呼叫到子類的方法。
其實在Java開發中,還有另外一個方法可以實現同樣的功能或是效果,那就是——Java回撥技術,通過回撥在介面中定義的方法,呼叫到具體的實現類中的方法,其本質同樣是利用Java的動態繫結技術,在這種實現中,可以不把實現類寫成單獨的類,而是使用匿名內部類來實現回撥方法。
應用Java回撥來實現模板方法模式,在實際開發中使用得也非常多,就算是模板方法模式的一種變形實現吧。
還是來示例一下,這樣會更清楚。為了大家好對比理解,把前面用標準模板方法模式實現的例子,採用Java回撥來實現一下。
(1)先定義一個模板方法需要的回撥介面
在這個介面中需要把所有可以被擴充套件的方法都要定義出來。實現的時候,可以不擴充套件,直接轉調模板中的預設實現,但是不能不定義出來,因為是介面,不定義出來,對於想要擴充套件這些功能的地方就沒有辦法了。示例程式碼如下:
/**
* 登入控制的模板方法需要的回撥介面,需要把所有需要的介面方法都定義出來,
* 或者說是所有可以被擴充套件的方法都需要被定義出來
*/
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);
}
(2)這裡使用的LoginModel跟以前沒有任何變化,就不去贅述了。
(3)該來定義登入控制的模板了,它的變化相對較多,大致有以下一些:
不再是抽象的類了,所有的抽象方法都去掉了
對模板方法就是login的那個方法,新增一個引數,傳入回撥介面
在模板方法實現中,除了在模板中固定的實現外,所有可以被擴充套件的方法,都應該通過回撥介面進行呼叫
示例程式碼如下:
/**
* 登入控制的模板
*/
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;
}
}
(4)由於是直接在呼叫的地方傳入回撥的實現,通常可以通過匿名內部類的方式來實現回撥介面,當然實現成為具體類也是可以的。如果採用匿名內部類的方式來使用模板,那麼就不需要原來的NormalLogin和WorkerLogin了。
(5)寫個客戶端來測試看看,客戶端需要使用匿名內部類來實現回撥介面,並實現其中想要擴充套件的方法,示例程式碼如下:
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);
}
}
(6)簡單小結一下,對於模板方法模式的這兩種實現方式:
使用繼承的方式,抽象方法和具體實現的關係,是在編譯期間靜態決定的,是類級的關係;使用Java回撥,這個關係是在執行期間動態決定的,是物件級的關係。
相對而言,使用回撥機制會更靈活,因為Java是單繼承的,如果使用繼承的方式,對於子類而言,今後就不能繼承其它物件了,而使用回撥,是基於介面的。
從另一方面說,回撥機制是通過委託的方式來組合功能,它的耦合強度要比繼承低一些,這會給我們更多的靈活性。比如某些模板實現的方法,在回撥實現的時候可以不呼叫模板中的方法,而是呼叫其它實現中的某些功能,也就是說功能不再侷限在模板和回撥實現上了,可以更靈活組織功能。
相對而言,使用繼承方式會更簡單點,因為父類提供了實現的方法,子類如果不想擴充套件,那就不用管。如果使用回撥機制,回撥的介面需要把所有可能被擴充套件的方法都定義進去,這就導致實現的時候,不管你要不要擴充套件,你都要實現這個方法,哪怕你什麼都不做,只是轉調模板中已有的實現,都要寫出來。
事實上,在前面講命令模式的時候也提到了Java回撥,還通過退化命令模式來實現了Java回撥的功能,所以也有這樣的說法:命令模式可以作為模板方法模式的一種替代實現,那就是因為可以使用Java回撥來實現模板方法模式。
16.3.4 典型應用:排序
模板方法模式的一個非常典型的應用,就是實現排序的功能。至於有些朋友認為排序是策略模式的體現,這很值得商榷。先來看看在Java中排序功能的實現,然後再來說明為什麼排序的實現主要體現了模板方法模式,而非策略模式。
在java.util包中,有一個Collections類,它裡面實現了對列表排序的功能,它提供了一個靜態的sort方法,接受一個列表和一個Comparator介面的例項,這個方法實現的大致步驟是:
先把列表轉換成為物件陣列
通過Arrays的sort方法來對陣列進行排序,傳入Comparator介面的例項
然後再把排好序的陣列的資料設定回到原來的列表物件中去
這其中的演算法步驟是固定的,也就是演算法骨架是固定的了,只是其中具體