【行為型模式十六】模板方法模式-1(Template Method)

1 場景問題#

1.1 登錄控制##

幾乎所有的應用系統,都需要系統登錄控制的功能,有些系統甚至有多個登錄控制的功能,比如:普通用戶可以登錄前臺,進行相應的業務操作;而工作人員可以登錄后臺,進行相應的系統管理或業務處理。

現在有這么一個基于Web的企業級應用系統,需要實現這兩種登錄控制,直接使用不同的登錄頁面來區分它們,把基本的功能需求分別描述如下:

先看看普通用戶登錄前臺的登錄控制的功能:

前臺頁面:用戶能輸入用戶名和密碼;提交登錄請求,讓系統去進行登錄控制;

后臺:從數據庫獲取登錄人員的信息;

后臺:判斷從前臺傳遞過來的登錄數據,和數據庫中已有的數據是否匹配;

前臺Action:如果匹配就轉向首頁,如果不匹配就返回到登錄頁面,并顯示錯誤提示信息;

再來看看工作人員登錄后臺的登錄控制功能:

前臺頁面:用戶能輸入用戶名和密碼;提交登錄請求,讓系統去進行登錄控制;

后臺:從數據庫獲取登錄人員的信息;

后臺:把從前臺傳遞過來的密碼數據,使用相應的加密算法進行加密運算,得到加密后的密碼數據;

后臺:判斷從前臺傳遞過來的用戶名和加密后的密碼數據,和數據庫中已有的數據是否匹配;

前臺Action:如果匹配就轉向首頁,如果不匹配就返回到登錄頁面,并顯示錯誤提示信息;

說明:普通用戶和工作人員在數據庫里面是存儲在不同表里面的;當然也是不同的模塊來維護普通用戶的數據和工作人員的數據;另外工作人員的密碼是加密存放的。

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;
    }
}  
  1. 再看看工作人員登錄的邏輯處理部分,示例代碼如下:
/**
 * 工作人員登錄控制的邏輯處理
 */
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;
    }
}

1.3 有何問題##

看了上面的實現示例,是不是很簡單。但是,仔細看看,總會覺得有點問題,兩種登錄的實現太相似了,現在是完全分開,當作兩個獨立的模塊來實現的,如果今后要擴展功能,比如要添加“控制同一個編號同時只能登錄一次”的功能,那么兩個模塊都需要修改,是很麻煩的。而且,現在的實現中,也有很多相似的地方,顯得很重復。另外,具體的實現和判斷的步驟混合在一起,不利于今后變換功能,比如要變換加密算法等。

總之,上面的實現,有兩個很明顯的問題:一是重復或相似代碼太多;二是擴展起來很不方便

那么該怎么解決呢?該如何實現才能讓系統既靈活又能簡潔的實現需求功能呢?

2 解決方案#

2.1 模板方法模式來解決##

用來解決上述問題的一個合理的解決方案就是模板方法模式。那么什么是模板方法模式呢?

  1. 模板方法模式定義
模板方法模式定義
  1. 應用模板方法模式來解決的思路

仔細分析上面的問題,重復或相似代碼太多、擴展不方便,出現這些問題的原因在哪里?主要就是兩個實現是完全分開、相互獨立的,沒有從整體上進行控制。如果把兩個模塊合起來看,就會發現,那些重復或相似的代碼就應該被抽取出來,做成公共的功能,而不同的登錄控制就可以去擴展這些公共的功能。這樣一來,擴展的時候,如果出現有相同的功能,那就直接擴展公共功能就可以了。

使用模板方法模式,就可以很好的來實現上面的思路。分析上面兩個登錄控制模塊,會發現它們在實現上,有著大致相同的步驟,只是在每步具體的實現上,略微有些不同,因此,可以把這些運算步驟看作是算法的骨架,把具體的不同的步驟實現,延遲到子類去實現,這樣就可以通過子類來提供不同的功能實現了。

經過分析總結,登錄控制大致的邏輯判斷步驟如下:

根據登錄人員的編號去獲取相應的數據;

獲取對登錄人員填寫的密碼數據進行加密后的數據,如果不需要加密,那就是直接返回登錄人員填寫的密碼數據;

判斷登錄人員填寫的數據和從數據庫中獲取的數據是否匹配;

在這三個步驟里面,第一個和第三個步驟是必不可少的,而第二個步驟是可選的。那么就可以定義一個父類,在里面定義一個方法來定義這個算法骨架,這個方法就是模板方法,然后把父類無法確定的實現,延遲到具體的子類來實現就可以了。

通過這樣的方式,如果要修改加密的算法,那就在模板的子類里面重新覆蓋實現加密的方法就好了,完全不需要去改變父類的算法結構,就可以重新定義這些特定的步驟。

2.2 模式結構和說明##

模板方法模式的結構如圖所示:

模板方法模式的結構

AbstractClass:抽象類。用來定義算法骨架和原語操作,具體的子類通過重定義這些原語操作來實現一個算法的各個步驟。在這個類里面,還可以提供算法中通用的實現。

ConcreteClass:具體實現類。用來實現算法骨架中的某些步驟,完成跟特定子類相關的功能。

2.3 模板方法模式示例代碼##

  1. 先來看看AbstractClass的寫法,示例代碼如下:
/**
 * 定義模板方法、原語操作等的抽象類
 */
public abstract class AbstractClass {
    /**
     * 原語操作1,所謂原語操作就是抽象的操作,必須要由子類提供實現
     */
    public abstract void doPrimitiveOperation1();
    /**
     * 原語操作2
     */
    public abstract void doPrimitiveOperation2();
    /**
     * 模板方法,定義算法骨架
     */
    public final void templateMethod() {
       doPrimitiveOperation1();
       doPrimitiveOperation2();
    }
}
  1. 再看看具體實現類的寫法,示例代碼如下:
/**
 * 具體實現類,實現原語操作
 */
public class ConcreteClass extends AbstractClass {
    public void doPrimitiveOperation1() {
       //具體的實現
    }
    public void doPrimitiveOperation2() {
       //具體的實現
    }
}

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;
    }  
}
  1. 接下來定義公共的登錄控制算法骨架,示例代碼如下:
/**
 *  登錄控制的模板
 */
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;
    }
}
  1. 實現新的普通用戶登錄控制的邏輯處理,示例代碼如下:
/**
 * 普通用戶登錄控制的邏輯處理
 */
public class NormalLogin extends LoginTemplate{
    public LoginModel findLoginUser(String loginId) {
        // 這里省略具體的處理,僅做示意,返回一個有默認數據的對象
        LoginModel lm = new LoginModel();
        lm.setLoginId(loginId);
        lm.setPwd("testpwd");
        return lm;
    }
}
  1. 實現新的工作人員登錄控制的邏輯處理,示例代碼如下:
/**
 * 工作人員登錄控制的邏輯處理
 */
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;
    }
}
  1. 通過上面的示例,可以看出來,把原來的實現改成使用模板方法模式來實現,也并不困難,寫個客戶端測試一下,以便更好的體會,示例代碼如下:
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);
   }
}

運行結果示例如下:

使用MD5進行密碼加密
可以登錄工作平臺=true
可以進行普通人員登錄=false

3 模式講解#

3.1 認識模板方法模式##

  1. 模式的功能

模板方法的功能在于固定算法骨架,而讓具體算法實現可擴展。

這在實際應用中非常廣泛,尤其是在設計框架級功能的時候非常有用。框架定義好了算法的步驟,在合適的點讓開發人員進行擴展,實現具體的算法。比如在DAO實現中,設計通用的增刪改查功能,這個后面會給大家示例。

模板方法還額外提供了一個好處,就是可以控制子類的擴展。因為在父類里面定義好了算法的步驟,只是在某幾個固定的點才會調用到被子類實現的方法,因此也就只允許在這幾個點來擴展功能,這些個可以被子類覆蓋以擴展功能的方法通常被稱為“鉤子”方法,后面也會給大家示例。

  1. 為何不是接口

有的朋友可能會問一個問題,不是說在Java中應該盡量面向接口編程嗎,為何模板方法的模板是采用的抽象方法呢?

要回答這個問題,要首先搞清楚抽象類和接口的關系:

接口是一種特殊的抽象類,所有接口中的屬性自動是常量,也就是public final static的,而所有接口中的方法必須是抽象的;

抽象類,簡單點說是用abstract修飾的類。這里要特別注意的是抽象類和抽象方法的關系,記住兩句話:抽象類不一定包含抽象方法;有抽象方法的類一定是抽象類

抽象類和接口相比較,最大的特點就在于抽象類里面是可以有具體的實現方法的,而接口中所有的方法都是沒有具體的實現的

因此,雖然Java編程中倡導大家“面向接口編程”,并不是說就不再使用抽象類了,那么什么時候使用抽象類呢?

通常在“既要約束子類的行為,又要為子類提供公共功能”的時候使用抽象類;

按照這個原則來思考模板方法模式的實現,模板方法模式需要固定定義算法的骨架,這個骨架應該只有一份,算是一個公共的行為,但是里面具體的步驟的實現又可能是各不相同的,恰好符合選擇抽象類的原則。

把模板實現成為抽象類,為所有的子類提供了公共的功能,就是定義了具體的算法骨架;同時在模板里面把需要由子類擴展的具體步驟的算法定義成為抽象方法,要求子類去實現這些方法,這就約束了子類的行為。

因此綜合考慮,用抽象類來實現模板是一個很好的選擇。

  1. 變與不變

程序設計的一個很重要的思考點就是“變與不變”,也就是分析程序中哪些功能是可變的,哪些功能是不變的,然后把不變的部分抽象出來,進行公共的實現,把變化的部分分離出去,用接口來封裝隔離,或者是用抽象類來約束子類行為。

模板方法模式很好的體現了這一點。模板類實現的就是不變的方法和算法的骨架,而需要變化的地方,都通過抽象方法,把具體實現延遲到子類去了,而且還通過父類的定義來約束了子類的行為,從而使系統能有更好的復用性和擴展性。

  1. 好萊塢法則

什么是好萊塢法則呢?簡單點說,就是“不要找我們,我們會聯系你”

模板方法模式很好的體現了這一點,做為父類的模板會在需要的時候,調用子類相應的方法,也就是由父類來找子類,而不是讓子類來找父類

這其實也是一種反向的控制結構,按照通常的思路,是子類找父類才對,也就是應該是子類來調用父類的方法,因為父類根本就不知道子類,而子類是知道父類的,但是在模板方法模式里面,是父類來找子類,所以是一種反向的控制結構。

那么,在Java里面能實現這樣功能的理論依據在哪里呢?

理論依據就在于Java的動態綁定采用的是“后期綁定”技術,對于出現子類覆蓋父類方法的情況,在編譯時是看數據類型,運行時看實際的對象類型(new操作符后跟的構造方法是哪個類的),一句話:new誰就調用誰的方法。

因此在使用模板方法模式的時候,雖然用的數據類型是模板類型,但是在創建類實例的時候是創建的具體的子類的實例,因此調用的時候,會被動態綁定到子類的方法上去,從而實現反向控制。其實在寫父類的時候,它調用的方法是父類自己的抽象方法,只是在運行的時候被動態綁定到了子類的方法上。

  1. 擴展登錄控制

在使用模板方法模式實現過后,如果想要擴展新的功能,有如下幾種情況:

一種情況是只需要提供新的子類實現就可以了,比如想要切換不同的加密算法,現在是使用的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);
    }
}

運行看看,能實現功能嗎?好好測試體會一下,看看是如何擴展功能的。

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的,都是正確的寫法。

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);
}
  1. 這里使用的LoginModel跟以前沒有任何變化,就不去贅述了。

  2. 該來定義登錄控制的模板了,它的變化相對較多,大致有以下一些:

不再是抽象的類了,所有的抽象方法都去掉了;

對模板方法就是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;
    }
}
  1. 由于是直接在調用的地方傳入回調的實現,通常可以通過匿名內部類的方式來實現回調接口,當然實現成為具體類也是可以的。如果采用匿名內部類的方式來使用模板,那么就不需要原來的NormalLogin和WorkerLogin了。

  2. 寫個客戶端來測試看看,客戶端需要使用匿名內部類來實現回調接口,并實現其中想要擴展的方法,示例代碼如下:

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);
    }
}

運行一下,看看效果是不是跟前面采用繼承的方式實現的結果是一樣的,然后好好比較一下這兩種實現方式。

  1. 簡單小結一下,對于模板方法模式的這兩種實現方式:
  1. 使用繼承的方式,抽象方法和具體實現的關系,是在編譯期間靜態決定的,是類級的關系;
  2. 使用Java回調,這個關系是在運行期間動態決定的,是對象級的關系。

相對而言,使用回調機制會更靈活,因為Java是單繼承的,如果使用繼承的方式,對于子類而言,今后就不能繼承其它對象了,而使用回調,是基于接口的。

從另一方面說,回調機制是通過委托的方式來組合功能,它的耦合強度要比繼承低一些,這會給我們更多的靈活性。比如某些模板實現的方法,在回調實現的時候可以不調用模板中的方法,而是調用其它實現中的某些功能,也就是說功能不再局限在模板和回調實現上了,可以更靈活組織功能。

相對而言,使用繼承方式會更簡單點,因為父類提供了實現的方法,子類如果不想擴展,那就不用管。如果使用回調機制,回調的接口需要把所有可能被擴展的方法都定義進去,這就導致實現的時候,不管你要不要擴展,你都要實現這個方法,哪怕你什么都不做,只是轉調模板中已有的實現,都要寫出來。

事實上,在前面講命令模式的時候也提到了Java回調,還通過退化命令模式來實現了Java回調的功能,所以也有這樣的說法:命令模式可以作為模板方法模式的一種替代實現,那就是因為可以使用Java回調來實現模板方法模式

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容