設(shè)計(jì)模式拿點(diǎn)事-行為型模式-模板方法模式

模板方法模式是類的行為模式。準(zhǔn)備一個(gè)抽象類,將部分邏輯以具體方法以及具體構(gòu)造函數(shù)的形式體現(xiàn),然后聲明一些抽象方法來迫使子類實(shí)現(xiàn)剩余的邏輯,不同的子類可以有不同的方式實(shí)現(xiàn)這些抽象方法,從而對(duì)剩余的邏輯有不同的實(shí)現(xiàn)。這就是模板方法模式的用意。

模板方法模式基于繼承的代碼復(fù)用。

模板方法模式需要開發(fā)抽象類和具體子類設(shè)計(jì)師之間的協(xié)作。一個(gè)設(shè)計(jì)師負(fù)責(zé)給出一個(gè)算法的輪廓和骨架;另一個(gè)設(shè)計(jì)師負(fù)責(zé)給出這個(gè)算法的各個(gè)邏輯步驟。代表這些具體邏輯步驟的方法稱為基本方法(primitive method),而將這些基本方法匯總起來的方法叫做模板方法(template method),這個(gè)設(shè)計(jì)模式的名字就是從此而來的。

1. 模板方法的結(jié)構(gòu)

模板方法模式的靜態(tài)結(jié)構(gòu)圖

模板方法所代表的行為稱為頂級(jí)行為,其邏輯稱為頂級(jí)邏輯。這里設(shè)計(jì)到兩個(gè)角色:

  • 抽象模板(Abstract Template)角色

    • 定義一個(gè)或多個(gè)抽象行為,以便讓子類實(shí)現(xiàn),這些抽象操作叫做基本操作,它們是一個(gè)頂級(jí)邏輯的組成步驟。

    • 定義并實(shí)現(xiàn)了一個(gè)模板方法。這個(gè)模板方法一般是一個(gè)具體方法,他給出了一個(gè)頂級(jí)邏輯的骨架,而邏輯的組成步驟在相應(yīng)的抽象操作中,推遲到子類實(shí)現(xiàn),頂級(jí)邏輯也有可能調(diào)用一些具體方法。

  • 具體模板(Concrete Template)角色

    • 實(shí)現(xiàn)父類所定義的一個(gè)或多個(gè)抽象方法,它們是一個(gè)頂級(jí)邏輯的組成步驟。

    • 每一個(gè)抽象模板角色都可以有任意多個(gè)具體模板角色與之對(duì)應(yīng),而每一個(gè)具體模板角色都可以給出這些抽象方法(也就是頂級(jí)邏輯的組成步驟)的不同實(shí)現(xiàn),從而使得頂級(jí)邏輯的實(shí)現(xiàn)各不相同。

2. 模板方法的代碼實(shí)現(xiàn)

模板方法類,abstractMethod()、hookMethod()等基本方法是頂級(jí)邏輯(templateMethod())的組成步驟。

//抽象模板角色
public abstract class AbstractTemplate {
    //模板方法的頂級(jí)邏輯
    public void templateMethod() {
        abstractMethod();
        hookMethod();
        concreteMethod();
    }
    //基本方法(強(qiáng)制由子類實(shí)現(xiàn))
    protected abstract void abstractMethod();
    //基本方法(可選擇由子類實(shí)現(xiàn))
    protected void hookMethod(){}
    //公共方法(父類實(shí)現(xiàn))
    private final void concreteMethod(){
        //業(yè)務(wù)相關(guān)代碼
    }
}

具體模板角色類,實(shí)現(xiàn)了父類所聲明的基本方法,abstractMethod()方法所代表的就是強(qiáng)制子類實(shí)現(xiàn)的剩余邏輯,而hookMethod()方法是可選擇實(shí)現(xiàn)的邏輯,不是必須實(shí)現(xiàn)的。

//具體模板角色
public class ConcreteTemplate extends AbstractTemplate {
    //強(qiáng)制重寫父類的抽象方法
    @Override
    protected void abstractMethod() {
        //業(yè)務(wù)相關(guān)代碼
    }
    //選擇重寫父類的具體方法
    @Override
    protected void hookMethod() {
        //業(yè)務(wù)相關(guān)代碼
    }
}

模板方法的關(guān)鍵是:子類可以置換父類的可變部分,但是子類卻不可以改變模板方法所代表的頂級(jí)邏輯。

每當(dāng)定義一個(gè)新的子類時(shí),不要按照控制流程的思路去想,而應(yīng)該按照“責(zé)任”的思路去想。換言之,應(yīng)當(dāng)考慮哪些操作是必須置換掉的,哪些操作是可以置換掉的,以及哪些操作是不可以置換掉的。使用模板方法可以使這些邏輯變得清晰。

3. 模板方法模式中的方法

模板方法模式可以分為兩大類:模板方法和基本方法。

3.1 模板方法

  • 一個(gè)模板方法是定義在抽象類中的,把基本操作方法組合在一起形成一個(gè)總算法或一個(gè)總行為的方法。

  • 一個(gè)抽象類可以有任意多個(gè)模板方法,而不限于一個(gè)。每個(gè)模板方法都可以調(diào)用任意多個(gè)具體方法。

3.2 基本方法

基本方法又可以分為三種:抽象方法(Abstract Method)、具體方法(Concrete Method)、鉤子方法(Hook Method)。

  • 抽象方法:一個(gè)抽象方法由抽象類聲明,由具體子類實(shí)現(xiàn)。在Java語(yǔ)言中抽象方法使用abstract關(guān)鍵字。

  • 具體方法:一個(gè)具體方法由抽象類聲明實(shí)現(xiàn),而子類并不實(shí)現(xiàn)或置換。

  • 鉤子方法:一個(gè)鉤子方法由抽象類聲明并實(shí)現(xiàn),而子類會(huì)加以擴(kuò)展,通常抽象類給出的實(shí)現(xiàn)是一個(gè)空實(shí)現(xiàn),作為方法的默認(rèn)實(shí)現(xiàn)。

3.3 默認(rèn)鉤子方法

一個(gè)鉤子方法常常由抽象類給出一個(gè)空實(shí)現(xiàn)作為此方法的默認(rèn)實(shí)現(xiàn)。這個(gè)空的鉤子方法叫做“Do Nothing Hook”,顯然,這種默認(rèn)鉤子方法在缺省適配模式里面已經(jīng)見過了,一個(gè)缺省適配模式將的是一個(gè)類為一個(gè)接口提供一個(gè)默認(rèn)的空實(shí)現(xiàn),從而使得缺省適配類的子類不必像實(shí)現(xiàn)接口那個(gè)必須給出所有方法的實(shí)現(xiàn),因?yàn)橥ǔR粋€(gè)具體類并不需要所有的方法。

3.4 鉤子方法的命名規(guī)則

鉤子方法的名字應(yīng)當(dāng)以do開始,這是熟悉設(shè)計(jì)模式的java開發(fā)人員的標(biāo)準(zhǔn)做法。在上面的例子中,鉤子方法hookMethod()應(yīng)當(dāng)以do開頭,在httpServlet類中,也遵從這一命名規(guī)則,如doGet()、doPost()等方法。

4. 模板設(shè)計(jì)模式的優(yōu)缺點(diǎn)

4.1 優(yōu)點(diǎn)

  • 提高代碼的復(fù)用性。
    將相同部分的代碼放在抽象的父類中。
  • 提高了擴(kuò)展性。
    將不同的代碼放到了不同的子類中,通過子類擴(kuò)展新的行為。
  • 實(shí)現(xiàn)了開放封閉原則。
    通過一個(gè)父類調(diào)用其子類的操作,通過對(duì)子類的擴(kuò)展增加新的行為,實(shí)現(xiàn)了“開放-封閉”原則。

4.2 缺點(diǎn)

引入了抽象類,每個(gè)不同的實(shí)現(xiàn)都需要一個(gè)子類來實(shí)現(xiàn),導(dǎo)致類的個(gè)數(shù)增加,增加了系統(tǒng)實(shí)現(xiàn)的復(fù)雜性。

5. 應(yīng)用場(chǎng)景

  • 一次性實(shí)現(xiàn)一個(gè)算法不變的部分,并將可變的行為留給子類來實(shí)現(xiàn)。
  • 各子類中公共的行為應(yīng)被提取出來并集中到一個(gè)公共父類中以避免代碼的重復(fù)。
  • 控制子類的擴(kuò)展。

5.1 銀行利息計(jì)算

考慮一個(gè)計(jì)算存款利息的例子。假設(shè)系統(tǒng)需要支持兩種存款賬號(hào),即貨幣市場(chǎng)(Money Market)賬號(hào)和定期存款(Certificate of Deposite)賬號(hào)。這兩種賬號(hào)的存款利息是不同的,因此,在計(jì)算一個(gè)存戶的存款利息額時(shí),必須區(qū)分兩種不同的賬號(hào)類型。

這個(gè)系統(tǒng)的總行為應(yīng)當(dāng)是計(jì)算出利息,那么就決定了作為一個(gè)模板方法模式的頂級(jí)邏輯應(yīng)當(dāng)是利息計(jì)算。利息計(jì)算涉及到兩個(gè)步驟:一是基本方法給出賬戶種類,另一個(gè)基本方法給出利息的百分比。這兩個(gè)基本方法構(gòu)成具體邏輯,因?yàn)橘~號(hào)的類型不同,所以具體邏輯會(huì)有所不同。

系統(tǒng)類結(jié)構(gòu)圖

抽象模板角色

//抽象模板角色
public abstract class Account {
    //模板方法
    public final double calulateInterest() {
        //計(jì)算利率
        double interestRate = doCalulateInterestRate();
        String accountType = doCalulateAccountType();
        //計(jì)算賬戶余額
        double amount = calulateAmount(accountType);
        //返回利息
        return amount * interestRate;
    }

    //查看卡片信息
    protected abstract String doCalulateAccountType();

    //計(jì)算利率
    protected abstract double doCalulateInterestRate();

    //基本方法,已經(jīng)實(shí)現(xiàn)
    private double calulateAmount(String accountType) {
        //省略邏輯代碼
        return 100.00;
    }
}

具體子類角色

public class MoneyMarketAccount extends Account{
    @Override
    protected String doCalulateAccountType() {
        return "Money Market";
    }

    @Override
    protected double doCalulateInterestRate() {
        return 0.045;
    }
}

客戶端

public class AccountClient {
    public static void main(String[] args) {
        Account account=new MoneyMarketAccount();
        double interest = account.calulateInterest();
        System.out.println("現(xiàn)金賬戶的利息是"+interest+"元");
    }
}

5.2 模板方法在Servlet中的應(yīng)用

使用Servlet需要繼承一個(gè)HttpServlet的抽象類,HttpServlet類提供了一個(gè)service()方法,這個(gè)方法調(diào)用七個(gè)doXXX()方法中的一個(gè)或多個(gè),完成對(duì)客戶端調(diào)用的響應(yīng)。這些doXXX()方法需要由HttpServlet的具體子類提供,因此是典型的模板方法模式。

 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);
    }
    }
Servlet的結(jié)構(gòu)

我們可以通過重寫父類的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擔(dān)任抽象模板角色:

  • 模板方法:由service()方法擔(dān)任。

  • 基本方法:由doPost()、doGet()等方法擔(dān)任。

TestServlet擔(dān)任具體模板角色:

TestServlet置換掉了父類HttpServlet中七個(gè)基本方法中的其中兩個(gè),分別是doGet()和doPost()。

參考實(shí)現(xiàn)

1.《JAVA與模式》之模板方法模式

2. 模板方法模式的優(yōu)缺點(diǎn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容