在面向?qū)ο蟪绦蛟O(shè)計(jì)過程中,程序員常常會(huì)遇到這種情況:設(shè)計(jì)一個(gè)系統(tǒng)時(shí)知道了算法所需的關(guān)鍵步驟,而且確定了這些步驟的執(zhí)行順序,但某些步驟的具體實(shí)現(xiàn)還未知,或者說某些步驟的實(shí)現(xiàn)與具體的環(huán)境相關(guān)。
例如,去銀行辦理業(yè)務(wù)一般要經(jīng)過以下4個(gè)流程:取號(hào)、排隊(duì)、辦理具體業(yè)務(wù)、對(duì)銀行工作人員進(jìn)行評(píng)分等,其中取號(hào)、排隊(duì)和對(duì)銀行工作人員進(jìn)行評(píng)分的業(yè)務(wù)對(duì)每個(gè)客戶是一樣的,可以在父類中實(shí)現(xiàn),但是辦理具體業(yè)務(wù)卻因人而異,它可能是存款、取款或者轉(zhuǎn)賬等,可以延遲到子類中實(shí)現(xiàn)。
這樣的例子在生活中還有很多,例如,一個(gè)人每天會(huì)起床、吃飯、做事、睡覺等,其中“做事”的內(nèi)容每天可能不同。我們把這些規(guī)定了流程或格式的實(shí)例定義成模板,允許使用者根據(jù)自己的需求去更新它,例如,簡(jiǎn)歷模板、論文模板、Word 中模板文件等。
以下介紹的模板方法模式將解決以上類似的問題。
模板方法(Template Method)模式的定義如下:定義一個(gè)操作中的算法骨架,而將算法的一些步驟延遲到子類中,使得子類可以不改變?cè)撍惴ńY(jié)構(gòu)的情況下重定義該算法的某些特定步驟。它是一種類行為型模式。
該模式的主要優(yōu)點(diǎn)如下。
① 它封裝了不變部分,擴(kuò)展可變部分。它把認(rèn)為是不變部分的算法封裝到父類中實(shí)現(xiàn),而把可變部分算法由子類繼承實(shí)現(xiàn),便于子類繼續(xù)擴(kuò)展。
② 它在父類中提取了公共的部分代碼,便于代碼復(fù)用。
③ 部分方法是由子類實(shí)現(xiàn)的,因此子類可以通過擴(kuò)展方式增加相應(yīng)的功能,符合開閉原則。
該模式的主要缺點(diǎn)如下。
① 對(duì)每個(gè)不同的實(shí)現(xiàn)都需要定義一個(gè)子類,這會(huì)導(dǎo)致類的個(gè)數(shù)增加,系統(tǒng)更加龐大,設(shè)計(jì)也更加抽象。
② 父類中的抽象方法由子類實(shí)現(xiàn),子類執(zhí)行的結(jié)果會(huì)影響父類的結(jié)果,這導(dǎo)致一種反向的控制結(jié)構(gòu),它提高了代碼閱讀的難度。
模板方法模式需要注意抽象類與具體子類之間的協(xié)作。它用到了虛函數(shù)的多態(tài)性技術(shù)以及“不用調(diào)用我,讓我來調(diào)用你”的反向控制技術(shù)。現(xiàn)在來介紹它們的基本結(jié)構(gòu)。
模板方法模式包含以下主要角色。
(1) 抽象類(Abstract Class):負(fù)責(zé)給出一個(gè)算法的輪廓和骨架。它由一個(gè)模板方法和若干個(gè)基本方法構(gòu)成。這些方法的定義如下。
① 模板方法:定義了算法的骨架,按某種順序調(diào)用其包含的基本方法。
② 基本方法:是整個(gè)算法中的一個(gè)步驟,包含以下幾種類型。
? 抽象方法:在抽象類中申明,由具體子類實(shí)現(xiàn)。
? 具體方法:在抽象類中已經(jīng)實(shí)現(xiàn),在具體子類中可以繼承或重寫它。
? 鉤子方法:在抽象類中已經(jīng)實(shí)現(xiàn),包括用于判斷的邏輯方法和需要子類重寫的空方法兩種。
(2) 具體子類(Concrete Class):實(shí)現(xiàn)抽象類中所定義的抽象方法和鉤子方法,它們是一個(gè)頂級(jí)邏輯的一個(gè)組成步驟。
模板方法模式的結(jié)構(gòu)圖如圖 1 所示。
圖1 模板方法模式的結(jié)構(gòu)圖
模板方法模式的代碼如下:
package templateMethod;
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)用...");
}
public abstract void abstractMethod1(); //抽象方法1
public abstract void abstractMethod2(); //抽象方法2
}
//具體子類
class ConcreteClass extends AbstractClass
{
public void abstractMethod1()
{
System.out.println("抽象方法1的實(shí)現(xiàn)被調(diào)用...");
}
public void abstractMethod2()
{
System.out.println("抽象方法2的實(shí)現(xiàn)被調(diào)用...");
}
}
程序的運(yùn)行結(jié)果如下:
抽象類中的具體方法被調(diào)用...
抽象方法1的實(shí)現(xiàn)被調(diào)用...
抽象方法2的實(shí)現(xiàn)被調(diào)用...
模式的應(yīng)用實(shí)例
【例1】用模板方法模式實(shí)現(xiàn)出國(guó)留學(xué)手續(xù)設(shè)計(jì)程序。
分析:出國(guó)留學(xué)手續(xù)一般經(jīng)過以下流程:索取學(xué)校資料,提出入學(xué)申請(qǐng),辦理因私出國(guó)護(hù)照、出境卡和公證,申請(qǐng)簽證,體檢、訂機(jī)票、準(zhǔn)備行裝,抵達(dá)目標(biāo)學(xué)校等,其中有些業(yè)務(wù)對(duì)各個(gè)學(xué)校是一樣的,但有些業(yè)務(wù)因?qū)W校不同而不同,所以比較適合用模板方法模式來實(shí)現(xiàn)。
在本實(shí)例中,我們先定義一個(gè)出國(guó)留學(xué)的抽象類 StudyAbroad,里面包含了一個(gè)模板方法 TemplateMethod(),該方法中包含了辦理出國(guó)留學(xué)手續(xù)流程中的各個(gè)基本方法,其中有些方法的處理由于各國(guó)都一樣,所以在抽象類中就可以實(shí)現(xiàn),但有些方法的處理各國(guó)是不同的,必須在其具體子類(如美國(guó)留學(xué)類 StudyInAmerica)中實(shí)現(xiàn)。如果再增加一個(gè)國(guó)家,只要增加一個(gè)子類就可以了,圖 2 所示是其結(jié)構(gòu)圖。
圖2 出國(guó)留學(xué)手續(xù)設(shè)計(jì)程序的結(jié)構(gòu)圖
程序代碼如下:
package templateMethod;
public class StudyAbroadProcess
{
public static void main(String[] args)
{
StudyAbroad tm=new StudyInAmerica();
tm.TemplateMethod();
}
}
//抽象類: 出國(guó)留學(xué)
abstract class StudyAbroad
{
public void TemplateMethod() //模板方法
{
LookingForSchool(); //索取學(xué)校資料
ApplyForEnrol(); //入學(xué)申請(qǐng)
ApplyForPassport(); //辦理因私出國(guó)護(hù)照、出境卡和公證
ApplyForVisa(); //申請(qǐng)簽證
ReadyGoAbroad(); //體檢、訂機(jī)票、準(zhǔn)備行裝
Arriving(); //抵達(dá)
}
public void ApplyForPassport()
{
System.out.println("三.辦理因私出國(guó)護(hù)照、出境卡和公證:");
System.out.println(" 1)持錄取通知書、本人戶口簿或身份證向戶口所在地公安機(jī)關(guān)申請(qǐng)辦理因私出國(guó)護(hù)照和出境卡。");
System.out.println(" 2)辦理出生公證書,學(xué)歷、學(xué)位和成績(jī)公證,經(jīng)歷證書,親屬關(guān)系公證,經(jīng)濟(jì)擔(dān)保公證。");
}
public void ApplyForVisa()
{
System.out.println("四.申請(qǐng)簽證:");
System.out.println(" 1)準(zhǔn)備申請(qǐng)國(guó)外境簽證所需的各種資料,包括個(gè)人學(xué)歷、成績(jī)單、工作經(jīng)歷的證明;個(gè)人及家庭收入、資金和財(cái)產(chǎn)證明;家庭成員的關(guān)系證明等;");
System.out.println(" 2)向擬留學(xué)國(guó)家駐華使(領(lǐng))館申請(qǐng)入境簽證。申請(qǐng)時(shí)需按要求填寫有關(guān)表格,遞交必需的證明材料,繳納簽證。有的國(guó)家(比如美國(guó)、英國(guó)、加拿大等)在申請(qǐng)簽證時(shí)會(huì)要求申請(qǐng)人前往使(領(lǐng))館進(jìn)行面試。");
}
public void ReadyGoAbroad()
{
System.out.println("五.體檢、訂機(jī)票、準(zhǔn)備行裝:");
System.out.println(" 1)進(jìn)行身體檢查、免疫檢查和接種傳染病疫苗;");
System.out.println(" 2)確定機(jī)票時(shí)間、航班和轉(zhuǎn)機(jī)地點(diǎn)。");
}
public abstract void LookingForSchool();//索取學(xué)校資料
public abstract void ApplyForEnrol(); //入學(xué)申請(qǐng)
public abstract void Arriving(); //抵達(dá)
}
//具體子類: 美國(guó)留學(xué)
class StudyInAmerica extends StudyAbroad
{
@Override
public void LookingForSchool()
{
System.out.println("一.索取學(xué)校以下資料:");
System.out.println(" 1)對(duì)留學(xué)意向國(guó)家的政治、經(jīng)濟(jì)、文化背景和教育體制、學(xué)術(shù)水平進(jìn)行較為全面的了解;");
System.out.println(" 2)全面了解和掌握國(guó)外學(xué)校的情況,包括歷史、學(xué)費(fèi)、學(xué)制、專業(yè)、師資配備、教學(xué)設(shè)施、學(xué)術(shù)地位、學(xué)生人數(shù)等;");
System.out.println(" 3)了解該學(xué)校的住宿、交通、醫(yī)療保險(xiǎn)情況如何;");
System.out.println(" 4)該學(xué)校在中國(guó)是否有授權(quán)代理招生的留學(xué)中介公司?");
System.out.println(" 5)掌握留學(xué)簽證情況;");
System.out.println(" 6)該國(guó)政府是否允許留學(xué)生合法打工?");
System.out.println(" 8)畢業(yè)之后可否移民?");
System.out.println(" 9)文憑是否受到我國(guó)認(rèn)可?");
}
@Override
public void ApplyForEnrol()
{
System.out.println("二.入學(xué)申請(qǐng):");
System.out.println(" 1)填寫報(bào)名表;");
System.out.println(" 2)將報(bào)名表、個(gè)人學(xué)歷證明、最近的學(xué)習(xí)成績(jī)單、推薦信、個(gè)人簡(jiǎn)歷、托福或雅思語言考試成績(jī)單等資料寄往所申請(qǐng)的學(xué)校;");
System.out.println(" 3)為了給簽證辦理留有充裕的時(shí)間,建議越早申請(qǐng)?jiān)胶茫话闾崆?年就比較從容。");
}
@Override
public void Arriving()
{
System.out.println("六.抵達(dá)目標(biāo)學(xué)校:");
System.out.println(" 1)安排住宿;");
System.out.println(" 2)了解校園及周邊環(huán)境。");
}
}
程序的運(yùn)行結(jié)果如下:
一.索取學(xué)校以下資料:
1)對(duì)留學(xué)意向國(guó)家的政治、經(jīng)濟(jì)、文化背景和教育體制、學(xué)術(shù)水平進(jìn)行較為全面的了解;
2)全面了解和掌握國(guó)外學(xué)校的情況,包括歷史、學(xué)費(fèi)、學(xué)制、專業(yè)、師資配備、教學(xué)設(shè)施、學(xué)術(shù)地位、學(xué)生人數(shù)等;
3)了解該學(xué)校的住宿、交通、醫(yī)療保險(xiǎn)情況如何;
4)該學(xué)校在中國(guó)是否有授權(quán)代理招生的留學(xué)中介公司?
5)掌握留學(xué)簽證情況;
6)該國(guó)政府是否允許留學(xué)生合法打工?
8)畢業(yè)之后可否移民?
9)文憑是否受到我國(guó)認(rèn)可?
二.入學(xué)申請(qǐng):
1)填寫報(bào)名表;
2)將報(bào)名表、個(gè)人學(xué)歷證明、最近的學(xué)習(xí)成績(jī)單、推薦信、個(gè)人簡(jiǎn)歷、托福或雅思語言考試成績(jī)單等資料寄往所申請(qǐng)的學(xué)校;
3)為了給簽證辦理留有充裕的時(shí)間,建議越早申請(qǐng)?jiān)胶茫话闾崆?年就比較從容。
三.辦理因私出國(guó)護(hù)照、出境卡和公證:
1)持錄取通知書、本人戶口簿或身份證向戶口所在地公安機(jī)關(guān)申請(qǐng)辦理因私出國(guó)護(hù)照和出境卡。
2)辦理出生公證書,學(xué)歷、學(xué)位和成績(jī)公證,經(jīng)歷證書,親屬關(guān)系公證,經(jīng)濟(jì)擔(dān)保公證。
四.申請(qǐng)簽證:
1)準(zhǔn)備申請(qǐng)國(guó)外境簽證所需的各種資料,包括個(gè)人學(xué)歷、成績(jī)單、工作經(jīng)歷的證明;個(gè)人及家庭收入、資金和財(cái)產(chǎn)證明;家庭成員的關(guān)系證明等;
2)向擬留學(xué)國(guó)家駐華使(領(lǐng))館申請(qǐng)入境簽證。申請(qǐng)時(shí)需按要求填寫有關(guān)表格,遞交必需的證明材料,繳納簽證。有的國(guó)家(比如美國(guó)、英國(guó)、加拿大等)在申請(qǐng)簽證時(shí)會(huì)要求申請(qǐng)人前往使(領(lǐng))館進(jìn)行面試。
五.體檢、訂機(jī)票、準(zhǔn)備行裝:
1)進(jìn)行身體檢查、免疫檢查和接種傳染病疫苗;
2)確定機(jī)票時(shí)間、航班和轉(zhuǎn)機(jī)地點(diǎn)。
六.抵達(dá)目標(biāo)學(xué)校:
1)安排住宿;
2)了解校園及周邊環(huán)境。
模式的應(yīng)用場(chǎng)景
模板方法模式通常適用于以下場(chǎng)景。
1、算法的整體步驟很固定,但其中個(gè)別部分易變時(shí),這時(shí)候可以使用模板方法模式,將容易變的部分抽象出來,供子類實(shí)現(xiàn)。
2、當(dāng)多個(gè)子類存在公共的行為時(shí),可以將其提取出來并集中到一個(gè)公共父類中以避免代碼重復(fù)。首先,要識(shí)別現(xiàn)有代碼中的不同之處,并且將不同之處分離為新的操作。最后,用一個(gè)調(diào)用這些新的操作的模板方法來替換這些不同的代碼。
3、當(dāng)需要控制子類的擴(kuò)展時(shí),模板方法只在特定點(diǎn)調(diào)用鉤子操作,這樣就只允許在這些點(diǎn)進(jìn)行擴(kuò)展。
在模板方法模式中,基本方法包含:抽象方法、具體方法和鉤子方法,正確使用“鉤子方法”可以使得子類控制父類的行為。如下面例子中,可以通過在具體子類中重寫鉤子方法 HookMethod1() 和 HookMethod2() 來改變抽象父類中的運(yùn)行結(jié)果,其結(jié)構(gòu)圖如圖 3 所示。
圖3 含鉤子方法的模板方法模式的結(jié)構(gòu)圖
程序代碼如下:
package templateMethod;
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)用...");
}
public void HookMethod1(){} //鉤子方法1
public boolean HookMethod2() //鉤子方法2
{
return true;
}
public abstract void abstractMethod1(); //抽象方法1
public abstract void abstractMethod2(); //抽象方法2
}
//含鉤子方法的具體子類
class HookConcreteClass extends HookAbstractClass
{
public void abstractMethod1()
{
System.out.println("抽象方法1的實(shí)現(xiàn)被調(diào)用...");
}
public void abstractMethod2()
{
System.out.println("抽象方法2的實(shí)現(xiàn)被調(diào)用...");
}
public void HookMethod1()
{
System.out.println("鉤子方法1被重寫...");
}
public boolean HookMethod2()
{
return false;
}
}
程序的運(yùn)行結(jié)果如下:
抽象方法1的實(shí)現(xiàn)被調(diào)用...
鉤子方法1被重寫...
抽象方法2的實(shí)現(xiàn)被調(diào)用...
如果鉤子方法 HookMethod1() 和鉤子方法 HookMethod2() 的代碼改變,則程序的運(yùn)行結(jié)果也會(huì)改變。