設(shè)計模式系列 — 模板方法模式

點贊再看唇兑,養(yǎng)成習(xí)慣,公眾號搜一搜【一角錢技術(shù)】關(guān)注更多原創(chuàng)技術(shù)文章。本文 GitHub org_hejianhui/JavaStudy 已收錄腕侄,有我的系列文章时呀。

前言

23種設(shè)計模式快速記憶的請看上面第一篇,本篇和大家一起來學(xué)習(xí)模板方法模式相關(guān)內(nèi)容晴玖。

模式定義

定義一個操作的算法骨架读存,而將一些步驟延遲到子類中为流。Template Method 使得子類可以不改變一個算法的結(jié)構(gòu)即可重定義該算法的某些特定步驟。

在不改變模板結(jié)構(gòu)的前提下在子類中重新定義模板中的內(nèi)容让簿。
模板方法模式是基于”繼承“的敬察;

模式實現(xiàn)如下:

package com.niuh.designpattern.templatemethod.v1;

/**
 * 模板方法模式
 */
public class TemplateMethodPattern {
    public static void main(String[] args) {
        AbstractClass tm = new ConcreteClass();
        tm.templateMethod();
    }
}

//抽象類
abstract class AbstractClass {

    //模板方法
    public void templateMethod() {
        specificMethod();
        abstractMethod1();
        abstractMethod2();
    }

    //具體方法
    public void specificMethod() {
        System.out.println("抽象類中的具體方法被調(diào)用...");
    }

    //抽象方法1
    public abstract void abstractMethod1();

    //抽象方法2
    public abstract void abstractMethod2();
}

//具體子類
class ConcreteClass extends AbstractClass {

    public void abstractMethod1() {
        System.out.println("抽象方法1的實現(xiàn)被調(diào)用...");
    }

    public void abstractMethod2() {
        System.out.println("抽象方法2的實現(xiàn)被調(diào)用...");
    }
}

輸出結(jié)果如下

抽象類中的具體方法被調(diào)用...
抽象方法1的實現(xiàn)被調(diào)用...
抽象方法2的實現(xiàn)被調(diào)用...

解決的問題

  • 提高代碼復(fù)用性

將相同部分的代碼放在抽象的父類中,而將不同的代碼放入不同的子類中

  • 實現(xiàn)了反向控制

通過一個父類調(diào)用其子類的操作尔当,通過對子類的具體實現(xiàn)擴展不同的行為莲祸,實現(xiàn)了反向控制 & 符合“開閉原則”

在面向?qū)ο蟪绦蛟O(shè)計過程中,常常會遇到這種情況:設(shè)計一個系統(tǒng)時知道了算法所需的關(guān)鍵步驟椭迎,而且確定了這些步驟的執(zhí)行順序锐帜,但某些步驟的具體實現(xiàn)還未知,或者說某些步驟的實現(xiàn)與具體的環(huán)境相關(guān)侠碧。

模式組成

模板方法模式需要注意抽象類與具體子類之間的協(xié)作抹估。它用到了虛函數(shù)的多態(tài)性技術(shù)以及“不用調(diào)用我,讓我來調(diào)用你”的反向控制技術(shù)∨担現(xiàn)在來介紹它們的基本結(jié)構(gòu)药蜻。

實例說明

實例概況

用模板方法模式實現(xiàn)出國留學(xué)手續(xù)設(shè)計程序。

分析:出國留學(xué)手續(xù)一般經(jīng)過以下流程:索取學(xué)校資料替饿,提出入學(xué)申請语泽,辦理因私出國護(hù)照、出境卡和公證视卢,申請簽證踱卵,體檢、訂機票据过、準(zhǔn)備行裝惋砂,抵達(dá)目標(biāo)學(xué)校等,其中有些業(yè)務(wù)對各個學(xué)校是一樣的绳锅,但有些業(yè)務(wù)因?qū)W校不同而不同西饵,所以比較適合用模板方法模式來實現(xiàn)。

在本實例中鳞芙,我們先定義一個出國留學(xué)的抽象類 StudyAbroad眷柔,里面包含了一個模板方法 TemplateMethod(),該方法中包含了辦理出國留學(xué)手續(xù)流程中的各個基本方法原朝,其中有些方法的處理由于各國都一樣驯嘱,所以在抽象類中就可以實現(xiàn),但有些方法的處理各國是不同的喳坠,必須在其具體子類(如美國留學(xué)類 StudyInAmerica)中實現(xiàn)鞠评。如果再增加一個國家,只要增加一個子類就可以了丙笋,圖 2 所示是其結(jié)構(gòu)圖谢澈。

使用步驟

步驟1:定義一個出國留學(xué)的抽象類 StudyAbroad

abstract class StudyAbroad {
    //模板方法
    public void templateMethod() {
        lookingForSchool(); //索取學(xué)校資料
        applyForEnrol();    //入學(xué)申請
        applyForPassport(); //辦理因私出國護(hù)照煌贴、出境卡和公證
        applyForVisa();     //申請簽證
        readyGoAbroad();    //體檢、訂機票锥忿、準(zhǔn)備行裝
        arriving();         //抵達(dá)
    }

    public void applyForPassport() {
        System.out.println("三.辦理因私出國護(hù)照牛郑、出境卡和公證:");
        System.out.println("  1)持錄取通知書、本人戶口簿或身份證向戶口所在地公安機關(guān)申請辦理因私出國護(hù)照和出境卡敬鬓。");
        System.out.println("  2)辦理出生公證書淹朋,學(xué)歷、學(xué)位和成績公證钉答,經(jīng)歷證書础芍,親屬關(guān)系公證,經(jīng)濟擔(dān)保公證数尿。");
    }

    public void applyForVisa() {
        System.out.println("四.申請簽證:");
        System.out.println("  1)準(zhǔn)備申請國外境簽證所需的各種資料仑性,包括個人學(xué)歷、成績單右蹦、工作經(jīng)歷的證明诊杆;個人及家庭收入、資金和財產(chǎn)證明何陆;家庭成員的關(guān)系證明等晨汹;");
        System.out.println("  2)向擬留學(xué)國家駐華使(領(lǐng))館申請入境簽證。申請時需按要求填寫有關(guān)表格贷盲,遞交必需的證明材料淘这,繳納簽證。有的國家(比如美國巩剖、英國铝穷、加拿大等)在申請簽證時會要求申請人前往使(領(lǐng))館進(jìn)行面試。");
    }

    public void readyGoAbroad() {
        System.out.println("五.體檢佳魔、訂機票氧骤、準(zhǔn)備行裝:");
        System.out.println("  1)進(jìn)行身體檢查、免疫檢查和接種傳染病疫苗吃引;");
        System.out.println("  2)確定機票時間、航班和轉(zhuǎn)機地點刽锤。");
    }

    //索取學(xué)校資料
    public abstract void lookingForSchool();

    //入學(xué)申請
    public abstract void applyForEnrol();

    //抵達(dá)
    public abstract void arriving();
}

步驟2:定義具體子類: 美國留學(xué)

class StudyInAmerica extends StudyAbroad {
    
    @Override
    public void lookingForSchool() {
        System.out.println("一.索取學(xué)校以下資料:");
        System.out.println("  1)對留學(xué)意向國家的政治镊尺、經(jīng)濟、文化背景和教育體制并思、學(xué)術(shù)水平進(jìn)行較為全面的了解庐氮;");
        System.out.println("  2)全面了解和掌握國外學(xué)校的情況,包括歷史宋彼、學(xué)費弄砍、學(xué)制仙畦、專業(yè)、師資配備音婶、教學(xué)設(shè)施慨畸、學(xué)術(shù)地位、學(xué)生人數(shù)等衣式;");
        System.out.println("  3)了解該學(xué)校的住宿寸士、交通钩骇、醫(yī)療保險情況如何砾隅;");
        System.out.println("  4)該學(xué)校在中國是否有授權(quán)代理招生的留學(xué)中介公司利职?");
        System.out.println("  5)掌握留學(xué)簽證情況准脂;");
        System.out.println("  6)該國政府是否允許留學(xué)生合法打工馏段?");
        System.out.println("  8)畢業(yè)之后可否移民耘成?");
        System.out.println("  9)文憑是否受到我國認(rèn)可且预?");
    }

    @Override
    public void applyForEnrol() {
        System.out.println("二.入學(xué)申請:");
        System.out.println("  1)填寫報名表收奔;");
        System.out.println("  2)將報名表荧飞、個人學(xué)歷證明凡人、最近的學(xué)習(xí)成績單、推薦信垢箕、個人簡歷划栓、托福或雅思語言考試成績單等資料寄往所申請的學(xué)校条获;");
        System.out.println("  3)為了給簽證辦理留有充裕的時間忠荞,建議越早申請越好,一般提前1年就比較從容帅掘。");
    }

    @Override
    public void arriving() {
        System.out.println("六.抵達(dá)目標(biāo)學(xué)校:");
        System.out.println("  1)安排住宿委煤;");
        System.out.println("  2)了解校園及周邊環(huán)境。");
    }
}

步驟3:測試結(jié)果

/**
 * 模板方法模式案例
 * <p>
 * 用模板方法模式實現(xiàn)出國留學(xué)手續(xù)設(shè)計程序
 */
public class TemplateMethodPattern {

    public static void main(String[] args) {
        StudyAbroad tm=new StudyInAmerica();
        tm.templateMethod();
    }
}

輸出結(jié)果

一.索取學(xué)校以下資料:
  1)對留學(xué)意向國家的政治修档、經(jīng)濟碧绞、文化背景和教育體制、學(xué)術(shù)水平進(jìn)行較為全面的了解吱窝;
  2)全面了解和掌握國外學(xué)校的情況讥邻,包括歷史、學(xué)費院峡、學(xué)制兴使、專業(yè)、師資配備照激、教學(xué)設(shè)施发魄、學(xué)術(shù)地位、學(xué)生人數(shù)等;
  3)了解該學(xué)校的住宿励幼、交通汰寓、醫(yī)療保險情況如何;
  4)該學(xué)校在中國是否有授權(quán)代理招生的留學(xué)中介公司苹粟?
  5)掌握留學(xué)簽證情況有滑;
  6)該國政府是否允許留學(xué)生合法打工?
  8)畢業(yè)之后可否移民六水?
  9)文憑是否受到我國認(rèn)可俺孙?
二.入學(xué)申請:
  1)填寫報名表;
  2)將報名表掷贾、個人學(xué)歷證明睛榄、最近的學(xué)習(xí)成績單、推薦信想帅、個人簡歷场靴、托福或雅思語言考試成績單等資料寄往所申請的學(xué)校港准;
  3)為了給簽證辦理留有充裕的時間旨剥,建議越早申請越好,一般提前1年就比較從容浅缸。
三.辦理因私出國護(hù)照轨帜、出境卡和公證:
  1)持錄取通知書、本人戶口簿或身份證向戶口所在地公安機關(guān)申請辦理因私出國護(hù)照和出境卡衩椒。
  2)辦理出生公證書蚌父,學(xué)歷、學(xué)位和成績公證毛萌,經(jīng)歷證書苟弛,親屬關(guān)系公證,經(jīng)濟擔(dān)保公證阁将。
四.申請簽證:
  1)準(zhǔn)備申請國外境簽證所需的各種資料膏秫,包括個人學(xué)歷、成績單做盅、工作經(jīng)歷的證明缤削;個人及家庭收入、資金和財產(chǎn)證明吹榴;家庭成員的關(guān)系證明等僻他;
  2)向擬留學(xué)國家駐華使(領(lǐng))館申請入境簽證。申請時需按要求填寫有關(guān)表格腊尚,遞交必需的證明材料,繳納簽證满哪。有的國家(比如美國婿斥、英國劝篷、加拿大等)在申請簽證時會要求申請人前往使(領(lǐng))館進(jìn)行面試。
五.體檢民宿、訂機票娇妓、準(zhǔn)備行裝:
  1)進(jìn)行身體檢查、免疫檢查和接種傳染病疫苗活鹰;
  2)確定機票時間哈恰、航班和轉(zhuǎn)機地點。
六.抵達(dá)目標(biāo)學(xué)校:
  1)安排住宿志群;
  2)了解校園及周邊環(huán)境着绷。

優(yōu)點

  1. 它封裝了不變部分,擴展可變部分锌云。它把認(rèn)為是不變部分的算法封裝到父類中實現(xiàn)荠医,而把可變部分算法由子類繼承實現(xiàn),便于子類繼續(xù)擴展桑涎。
  2. 它在父類中提取了公共的部分代碼彬向,便于代碼復(fù)用。
  3. 部分方法是由子類實現(xiàn)的攻冷,因此子類可以通過擴展方式增加相應(yīng)的功能娃胆,符合開閉原則。

缺點

  1. 對每個不同的實現(xiàn)都需要定義一個子類等曼,這會導(dǎo)致類的個數(shù)增加里烦,系統(tǒng)更加龐大,設(shè)計也更加抽象涉兽。
  2. 父類中的抽象方法由子類實現(xiàn)招驴,子類執(zhí)行的結(jié)果會影響父類的結(jié)果,這導(dǎo)致一種反向的控制結(jié)構(gòu)枷畏,它提高了代碼閱讀的難度别厘。

應(yīng)用場景

  1. 當(dāng)你想讓客戶端只擴展算法的特定步驟,而不是整個算法或其結(jié)構(gòu)時拥诡,請使用Template Method模式触趴;
  2. 當(dāng)你有幾個類包含幾乎相同的算法,但有一些細(xì)微的差異時渴肉,請使用此模式冗懦。

模式的擴展

在模板方法模式中,基本方法包含:抽象方法仇祭、具體方法和鉤子方法披蕉,正確使用“鉤子方法”可以使得子類控制父類的行為。如下面例子中,可以通過在具體子類中重寫鉤子方法 HookMethod1() 和 HookMethod2() 來改變抽象父類中的運行結(jié)果没讲,其結(jié)構(gòu)圖如下圖所示:


代碼如下

package com.niuh.designpattern.templatemethod.v3;

/**
 * 含鉤子方法的模板方法模式
 */
public class HookTemplateMethod {
    public static void main(String[] args) {
        HookAbstractClass tm = new HookConcreteClass();
        tm.TemplateMethod();
    }
}

//含鉤子方法的抽象類
abstract class HookAbstractClass {
    //模板方法
    public void TemplateMethod() {
        abstractMethod1();
        HookMethod1();
        if (HookMethod2()) {
            SpecificMethod();
        }
        abstractMethod2();
    }

    //具體方法
    public void SpecificMethod() {
        System.out.println("抽象類中的具體方法被調(diào)用...");
    }

    //鉤子方法1
    public void HookMethod1() {
    }

    //鉤子方法2
    public boolean HookMethod2() {
        return true;
    }

    public abstract void abstractMethod1(); //抽象方法1

    public abstract void abstractMethod2(); //抽象方法2
}

//含鉤子方法的具體子類
class HookConcreteClass extends HookAbstractClass {
    public void abstractMethod1() {
        System.out.println("抽象方法1的實現(xiàn)被調(diào)用...");
    }

    public void abstractMethod2() {
        System.out.println("抽象方法2的實現(xiàn)被調(diào)用...");
    }

    public void HookMethod1() {
        System.out.println("鉤子方法1被重寫...");
    }

    public boolean HookMethod2() {
        return false;
    }
}

輸出結(jié)果如下:

抽象方法1的實現(xiàn)被調(diào)用...
鉤子方法1被重寫...
抽象方法2的實現(xiàn)被調(diào)用...

源碼中的應(yīng)用

#Servlet Api
javax.servlet.http.HttpServlet

#Spring
org.springframework.web.servlet.mvc.AbstractController
......

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

Servle除了要在web.xml做相應(yīng)的配置外眯娱,還需繼承一個叫HttpServlet的抽象類。HttpService類提供了一個service()方法爬凑,這個方法調(diào)用七個do方法中的一個或幾個徙缴,完成對客戶端調(diào)用的響應(yīng)。這些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()方法也可以被子類置換掉。下面給出一個簡單的Servlet例子:

image.png

從上面的類圖可以看出潘靖,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");
    }

}

從上面的例子可以看出這是一個典型的模板方法模式秘豹。

  • HttpServlet擔(dān)任抽象模板角色
    • 模板方法:由service()方法擔(dān)任携御。
    • 基本方法:由doPost()、doGet()等方法擔(dān)任既绕。
  • TestServlet擔(dān)任具體模板角色
    • TestServlet置換掉了父類HttpServlet中七個基本方法中的其中兩個啄刹,分別是doGet()和doPost()。

PS:以上代碼提交在 Githubhttps://github.com/Niuh-Study/niuh-designpatterns.git

文章持續(xù)更新凄贩,可以公眾號搜一搜「 一角錢技術(shù) 」第一時間閱讀誓军, 本文 GitHub org_hejianhui/JavaStudy 已經(jīng)收錄,歡迎 Star疲扎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末昵时,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子椒丧,更是在濱河造成了極大的恐慌壹甥,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件壶熏,死亡現(xiàn)場離奇詭異句柠,居然都是意外死亡,警方通過查閱死者的電腦和手機棒假,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進(jìn)店門溯职,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人帽哑,你說我怎么就攤上這事谜酒。” “怎么了妻枕?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵僻族,是天一觀的道長粘驰。 經(jīng)常有香客問我,道長鹰贵,這世上最難降的妖魔是什么晴氨? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮碉输,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘亭珍。我一直安慰自己敷钾,他們只是感情好肄梨,可當(dāng)我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著众羡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪粱侣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天齐婴,我揣著相機與錄音,去河邊找鬼柠偶。 笑死,一個胖子當(dāng)著我的面吹牛诱担,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蔫仙,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼料睛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了匀哄?” 一聲冷哼從身側(cè)響起秦效,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎涎嚼,沒想到半個月后阱州,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡法梯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年苔货,在試婚紗的時候發(fā)現(xiàn)自己被綠了犀概。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡夜惭,死狀恐怖姻灶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情诈茧,我是刑警寧澤产喉,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站敢会,受9級特大地震影響曾沈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鸥昏,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一塞俱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吏垮,春花似錦障涯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至旅敷,卻和暖如春生棍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背媳谁。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工涂滴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人晴音。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓柔纵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親锤躁。 傳聞我的和親對象是個殘疾皇子搁料,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,452評論 2 348