設計模式(2):行爲型-模板方法模式(Template Method)

設計模式(Design pattern)是一套被反覆使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結。使用設計模式是爲了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。 毫無疑問,設計模式於己於他人於系統都是多贏的;設計模式使代碼編制真正工程化;設計模式是軟件工程的基石脈絡,如同大廈的結構一樣。

設計模式分爲三種類型,共23種。
創建型模式(5):單例模式、抽象工廠模式、建造者模式、工廠模式、原型模式。
結構型模式(7):適配器模式、橋接模式、裝飾模式、組合模式、外觀模式、享元模式、代理模式。
行爲型模式(11):(父子類)策略模式、模版方法模式,(兩個類)觀察者模式、迭代器模式、職責鏈模式、命令模式,(類的狀態)狀態模式、備忘錄模式,(中間類) 訪問者模式、中介者模式、解釋器模式。

一.概述

準備一個抽象類,將部分邏輯以具體方法以及具體構造函數的形式實現(稱爲模板方法),然後聲明一些抽象方法來迫使子類實現剩餘的邏輯。不同的子類可以以不同的方式實現這些抽象方法,從而對剩餘的邏輯有不同的實現。這就是模板方法模式的用意。

定義

  模板方法模式:定義一個操作中算法的框架,而將一些步驟延遲到子類中。模板方法模式使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
  Template Method Pattern: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.
  模板方法模式是一種基於繼承的代碼複用技術,它是一種類行爲型模式。
  模板方法模式是結構最簡單的行爲型設計模式,在其結構中只存在父類與子類之間的繼承關係。通過使用模板方法模式,可以將一些複雜流程的實現步驟封裝在一系列基本方法中,在抽象父類中提供一個稱之爲模板方法的方法來定義這些基本方法的執行次序,而通過其子類來覆蓋某些步驟,從而使得相同的算法框架可以有不同的執行結果。模板方法模式提供了一個模板方法來定義算法框架,而某些具體步驟的實現可以在其子類中完成。

結構

模板方法模式結構比較簡單,其核心是抽象類和其中的模板方法的設計,其結構如圖所示:
這裏寫圖片描述
由圖可知,模板方法模式包含如下兩個角色:

  • AbstractClass(抽象類):在抽象類中定義了一系列基本操作(PrimitiveOperations),這些基本操作可以是具體的,也可以是抽象的,每一個基本操作對應算法的一個步驟,在其子類中可以重定義或實現這些步驟。同時,在抽象類中實現了一個模板方法(Template Method),用於定義一個算法的框架,模板方法不僅可以調用在抽象類中實現的基本方法,也可以調用在抽象類的子類中實現的基本方法,還可以調用其他對象中的方法。
  • ConcreteClass(具體子類):它是抽象類的子類,用於實現在父類中聲明的抽象基本操作以完成子類特定算法的步驟,也可以覆蓋在父類中已經實現的具體基本操作。

幾個概念

  在實現模板方法模式時,開發抽象類的軟件設計師和開發具體子類的軟件設計師之間可以進行協作。一個設計師負責給出一個算法的輪廓和框架,另一些設計師則負責給出這個算法的各個邏輯步驟。實現這些具體邏輯步驟的方法即爲基本方法,而將這些基本方法彙總起來的方法即爲模板方法,模板方法模式的名字也因此而來。下面將詳細介紹模板方法和基本方法:
  
模板方法: 一個模板方法是定義在抽象類中的、把基本操作方法組合在一起形成一個總算法或一個總行爲的方法。這個模板方法定義在抽象類中,並由子類不加以修改地完全繼承下來。模板方法是一個具體方法,它給出了一個頂層邏輯框架,而邏輯的組成步驟在抽象類中可以是具體方法,也可以是抽象方法。由於模板方法是具體方法,因此模板方法模式中的抽象層只能是抽象類,而不是接口。

基本方法: 基本方法是實現算法各個步驟的方法,是模板方法的組成部分。基本方法又可以分爲三種:抽象方法(Abstract Method)、具體方法(Concrete Method)和鉤子方法(Hook Method)。

  • 抽象方法:抽象方法由抽象類聲明,由具體子類實現。在Java語言裏抽象方法以abstract關鍵字標示。
  • 具體方法:一個具體方法由一個抽象類或具體類聲明並實現,其子類可以進行覆蓋也可以直接繼承。
  • 鉤子方法:一個鉤子方法由一個抽象類或具體類聲明並實現,而其子類可能會加以擴展。通常在父類中給出的實現是一個空實現(可使用virtual關鍵字將其定義爲虛函數),並以該空實現作爲方法的默認實現,當然鉤子方法也可以提供一個非空的默認實現。

默認鉤子方法:鉤子方法常常由抽象類給出一個空實現作爲此方法的默認實現。這種空的鉤子方法叫做“Do Nothing Hook”。顯然,這種默認鉤子方法在缺省適配模式裏面已經見過了,一個缺省適配模式講的是一個類爲一個接口提供一個默認的空實現,從而使得缺省適配類的子類不必像實現接口那樣必須給出所有方法的實現,因爲通常一個具體類並不需要所有的方法。

注意:鉤子方法的引入不是爲了改變父類的邏輯行爲。

實現

在模板方法模式中,抽象類的典型代碼如下:

abstract class AbstractClass   {    
    //模板方法    
    public void templateMethod(){    
        primitiveOperation1();    
        primitiveOperation2();    
        primitiveOperation3();    
    }    

    //基本方法—具體方法    
    public void primitiveOperation1()  {    
    //實現代碼    
    }    

    //基本方法—抽象方法    
    public abstract void primitiveOperation2();        

    //基本方法—鉤子方法    
    public void primitiveOperation3()       
    {  }    
}    

  在抽象類中,模板方法TemplateMethod()定義了算法的框架,在模板方法中調用基本方法以實現完整的算法,每一個基本方法如PrimitiveOperation1()、PrimitiveOperation2()等均實現了算法的一部分,對於所有子類都相同的基本方法可在父類提供具體實現,例如PrimitiveOperation1(),否則在父類聲明爲抽象方法或鉤子方法,由不同的子類提供不同的實現,例如PrimitiveOperation2()和PrimitiveOperation3()。
  可在抽象類的子類中提供抽象步驟的實現,也可覆蓋父類中已經實現的具體方法,具體子類的典型代碼如下:

public class ConcreteClass extends AbstractClass  {    
    public void primitiveOperation2(){    
    //實現代碼    
    }    

   public void primitiveOperation3()   {    
    //實現代碼    
   }    
}   

  在模板方法模式中,由於面向對象的多態性,子類對象在運行時將覆蓋父類對象,子類中定義的方法也將覆蓋父類中定義的方法,因此程序在運行時,具體子類的基本方法將覆蓋父類中定義的基本方法,子類的鉤子方法也將覆蓋父類的鉤子方法,從而可以通過在子類中實現的鉤子方法對父類方法的執行進行約束,實現子類對父類行爲的反向控制。

二.在Servlet中的應用

使用過Servlet的人都清楚,除了要在web.xml做相應的配置外,還需繼承一個叫HttpServlet的抽象類。HttpServlet類提供了一個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()方法也可以被子類置換掉。
可以用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");
    }

}

框架實際上調用的是service()方法,如果是GET請求,service()方法則調用TestServlet 的doGet方法。

三.總結

在模板方法模式中,子類不顯式調用父類的方法,而是通過覆蓋父類的模板方法來實現某些具體的業務邏輯,父類控制對子類的調用,這種機制被稱爲好萊塢原則(Hollywood Principle)
好萊塢原則的定義爲:“不要給我們打電話,我們會給你打電話(Don‘t call us, we’ll call you)”。在好萊塢,把簡歷遞交給演藝公司後就只有回家等待。由演藝公司對整個娛樂項的完全控制,演員只能被動式的接受公司的差使,在需要的環節中,完成自己的演出。
模板方法模式充分的體現了“好萊塢”原則。由父類完全控制着子類的邏輯,子類不需要調用父類,而通過父類來調用子類,子類可以實現父類的可變部份,卻繼承父類的邏輯,不能改變業務邏輯。

模板方法模式意圖是由抽象父類控制頂級邏輯,並把基本操作的實現推遲到子類去實現,這是通過繼承的手段來達到對象的複用。

鉤子方法的作用這裏值得探討下,個人認爲鉤子方法不應該控制父類的模板方法的邏輯,否則就破壞了這種模式的設計初衷,原則上子類是不應該也不需要知道父類的邏輯。它的作用應該像缺省適配模式下的子類不必像實現接口那樣必須給出所有方法的實現。

參考電子書下載:設計模式的藝術–軟件開發人員內功修煉之道_劉偉(2013年).pdf

《道德經》第五章:
天地不仁,以萬物爲芻(chu)狗;聖人不仁,以百姓爲芻狗。天地之間,其猶橐龠(tuoyue)乎?虛而不屈(gu),動而俞(俞)出。多聞數窮,不若守於中。
譯文:天地是無所謂仁慈的,它沒有仁愛,對待萬事萬物就像對待芻狗一樣,任憑萬物自生自滅。聖人也是沒有仁愛的,也同樣像芻狗那樣對待百姓,任憑人們自作自息。天地之間,豈不像個風箱一樣嗎?它空虛而不枯竭,越鼓動風就越多,生生不息。政令繁多反而更加使人困惑,更行不通,不如保持虛靜。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章