在面向對象程序設計過程中,程序員常常會遇到這種情況:設計一個系統時知道了算法所需的關鍵步驟,而且確定了這些步驟的執行順序,但某些步驟的具體實現還未知,或者說某些步驟的實現與具體的環境相關。
例如,去銀行辦理業務一般要經過以下4個流程:取號、排隊、辦理具體業務、對銀行工作人員進行評分等,其中取號、排隊和對銀行工作人員進行評分的業務對每個客戶是一樣的,可以在父類中實現,但是辦理具體業務卻因人而異,它可能是存款、取款或者轉賬等,可以延遲到子類中實現。
這樣的例子在生活中還有很多,例如,一個人每天會起牀、吃飯、做事、睡覺等,其中“做事”的內容每天可能不同。我們把這些規定了流程或格式的實例定義成模板,允許使用者根據自己的需求去更新它,例如,簡歷模板、論文模板、Word 中模板文件等。
以下介紹的模板方法模式將解決以上類似的問題。
模板的定義與特點
定義:
模板方法模式 定義一個操作中的算法的骨架,而將一些步驟延遲到子類中,使得子類可以不改變一個算法的結構,就可以重定義該算法的某些特定步驟
特點:
優點
- 它封裝了不變部分,擴展可變部分。它把認爲是不變部分的算法封裝到父類中實現,而把可變部分算法由子類繼承實現,便於子類繼續擴展。
- 它在父類中提取了公共的部分代碼,便於代碼複用。
- 部分方法是由子類實現的,因此子類可以通過擴展方式增加相應的功能,符合開閉原則。
缺點
- 對每個不同的實現都需要定義一個子類,這會導致類的個數增加,系統更加龐大,設計也更加抽象。
- 父類中的抽象方法由子類實現,子類執行的結果會影響父類的結果,這導致一種反向的控制結構,它提高了代碼閱讀的難度。
模板的結構
角色說明
(1) 抽象類(Abstract Class):負責給出一個算法的輪廓和骨架。它由一個模板方法和若干個基本方法構成。這些方法的定義如下。
① 模板方法:定義了算法的骨架,按某種順序調用其包含的基本方法。
② 基本方法:是整個算法中的一個步驟,包含以下幾種類型。
抽象方法:在抽象類中申明,由具體子類實現。
具體方法:在抽象類中已經實現,在具體子類中可以繼承或重寫它。
鉤子方法:在抽象類中已經實現,包括用於判斷的邏輯方法和需要子類重寫的空方法兩種。
(2) 具體子類(Concrete Class):實現抽象類中所定義的抽象方法和鉤子方法,它們是一個頂級邏輯的一個組成步驟。
模板方法實現
下面用模板方法實現製作豆漿的程序
分析:
製作豆漿的流程一般是: 選材—>添加配料—>浸泡—>放到豆漿機打碎。通過添加不同的配料,可以製作出不同口味的豆漿
選材、浸泡和放到豆漿機打碎這幾個步驟對於製作每種口味的豆漿都是一樣的(紅豆、花生豆漿。。。)
圖解
代碼實現
抽象類
//抽象類,表示豆漿
public abstract class SoyaMilk {
//模板方法, make , 模板方法可以做成 final , 不讓子類去覆蓋.
final void make() {
select();
addCondiments();
soak();
beat();
}
//選材料
void select() {
System.out.println("第一步:選擇好的新鮮黃豆 ");
}
//添加不同的配料, 抽象方法, 子類具體實現
abstract void addCondiments();
//浸泡
void soak() {
System.out.println("第三步, 黃豆和配料開始浸泡, 需要 3 小時 ");
}
//打豆漿
void beat() {
System.out.println("第四步:黃豆和配料放到豆漿機去打碎 ");
}
//鉤子方法,決定是否需要添加配料
boolean customerWantCondiments() {
return true;
}
}
具體子類
//具體子類 —— 花生豆漿類
public class RedBeanSoyaMilk extends SoyaMilk{
@Override
void addCondiments() {
System.out.println("加入紅豆配料");
}
}
//具體子類 —— 花生豆漿類
public class PeanutSoyaMilk extends SoyaMilk{
@Override
void addCondiments() {
System.out.println("加上花生配料");
}
}
客戶端
public class Client {
public static void main(String[] args) {
//製作紅豆豆漿
System.out.println("----製作紅豆豆漿----");
SoyaMilk redBeanSoyaMilk = new RedBeanSoyaMilk();
redBeanSoyaMilk.make();
System.out.println("---- 制 作 花 生 豆 漿 ----");
SoyaMilk peanutSoyaMilk = new PeanutSoyaMilk();
peanutSoyaMilk.make();
}
}
/*
----製作紅豆豆漿----
第一步:選擇好的新鮮黃豆
加入紅豆配料
第三步, 黃豆和配料開始浸泡, 需要 3 小時
第四步:黃豆和配料放到豆漿機去打碎
---- 制 作 花 生 豆 漿 ----
第一步:選擇好的新鮮黃豆
加上花生配料
第三步, 黃豆和配料開始浸泡, 需要 3 小時
第四步:黃豆和配料放到豆漿機去打碎
*/
模式的應用場景
- 算法的整體步驟很固定,但其中個別部分易變時,這時候可以使用模板方法模式,將容易變的部分抽象出來,供子類實現。
- 當多個子類存在公共的行爲時,可以將其提取出來並集中到一個公共父類中以避免代碼重複。首先,要識別現有代碼中的不同之處,並且將不同之處分離爲新的操作。最後,用一個調用這些新的操作的模板方法來替換這些不同的代碼。
- 當需要控制子類的擴展時,模板方法只在特定點調用鉤子操作,這樣就只允許在這些點進行擴展。
模式擴展
- 在模板方法模式的父類中,我們可以定義一個方法,它默認不做任何事,子類可以視情況要不要覆蓋它,該方法稱爲“鉤子”。
- 在模板方法模式中,基本方法包含:抽象方法、具體方法和鉤子方法,正確使用“鉤子方法”可以使得子類控制父類的行爲。
- 還是用上面做豆漿的例子來講解,比如,我們希望製作純豆漿,不添加任何的配料,請使用鉤子方法對前面的模板方法進行改造
//抽象類,表示豆漿
public abstract class SoyaMilk {
//模板方法, make , 模板方法可以做成 final , 不讓子類去覆蓋.
final void make() {
select();
if(customerWantCondiments()) {
addCondiments();
};
soak();
beat();
}
//選材料
void select() {
System.out.println("第一步:選擇好的新鮮黃豆 ");
}
//添加不同的配料, 抽象方法, 子類具體實現
abstract void addCondiments();
//浸泡
void soak() {
System.out.println("第三步, 黃豆和配料開始浸泡, 需要 3 小時 ");
}
//打豆漿
void beat() {
System.out.println("第四步:黃豆和配料放到豆漿機去打碎 ");
}
//鉤子方法,決定是否需要添加配料
boolean customerWantCondiments() {
return true;
}
}
//具體子類——純豆漿類
public class PureSoyaMilk extends SoyaMilk {
@Override
void addCondiments() {
}
//重寫父類的鉤子函數,讓添加原料方法不再執行
@Override
boolean customerWantCondiments() {
return false;
}
}
如果鉤子方法的代碼改變,則程序的運行結果也會改變。