公號:碼農充電站pro
主頁:https://codeshellme.github.io
今天來介紹模板方法模式(Template Method Design Pattern)。
1,製作飲料的過程
假如我們要製作兩種飲料:蘋果飲料和橙子飲料。這兩種飲料的製作流程如下:
- 蘋果飲料製作流程:
- 把蘋果榨成蘋果汁
- 將蘋果汁倒入杯中
- 向杯中倒入水
- 根據客戶喜好是否放入白糖
- 喜歡則放入白糖,否則不放白糖
- 橙子飲料製作流程:
- 把橙子榨成橙汁
- 將橙汁倒入杯中
- 向杯中倒入水
- 根據客戶喜好是否放入白糖
- 喜歡則放入白糖,否則不放白糖
2,模擬製作飲料
如果要模擬飲料的製作過程,按照最直接的想法,我們建立兩個類 AppleBeverage 和 OrangeBeverage 分別用於製作蘋果飲料和橙子飲料。
首先根據蘋果飲料的製作流程編寫 AppleBeverage 類:
class AppleBeverage {
private boolean isSweet;
public AppleBeverage(boolean isSweet) {
this.isSweet = isSweet;
}
// 把蘋果榨成蘋果汁
public void squeezeAppleJuice() {
System.out.println("squeeze apple juice");
}
// 將蘋果汁倒入杯中
public void appleJuiceToCup() {
System.out.println("pour the apple juice into the cup");
}
// 向杯中倒入水
public void waterToCup() {
System.out.println("pour water into the cup");
}
// 向杯中倒入白糖
public void sugarToCup() {
System.out.println("pour sugar into the cup");
}
// 製作蘋果飲料
public void makeAppleBeverage() {
squeezeAppleJuice();
appleJuiceToCup();
waterToCup();
if (isSweet) {
sugarToCup();
}
}
}
再根據橙子飲料的製作流程編寫 OrangeBeverage 類:
class OrangeBeverage {
private boolean isSweet;
public OrangeBeverage(boolean isSweet) {
this.isSweet = isSweet;
}
// 把橙子榨成橙汁
public void squeezeOrangeJuice() {
System.out.println("squeeze orange juice");
}
// 將橙汁倒入杯中
public void orangeJuiceToCup() {
System.out.println("pour the orange juice into the cup");
}
// 向杯中倒入水
public void waterToCup() {
System.out.println("pour water into the cup");
}
// 向杯中倒入白糖
public void sugarToCup() {
System.out.println("pour sugar into the cup");
}
// 製作橙子飲料
public void makeOrangeBeverage() {
squeezeOrangeJuice();
orangeJuiceToCup();
waterToCup();
if (isSweet) {
sugarToCup();
}
}
}
3,分析程式碼
可以看到上面兩個類的程式碼非常簡單,為了更加詳細的分析,我畫出了這兩個類的類圖:
我將這兩個類中的方法相同的部分用藍色標了出來,可以看到,這兩個類中的 waterToCup
和 sugarToCup
方法一模一樣,其它三個方法也是非常的相似。
這樣的程式碼顯然是沒有複用已有的程式碼。
4,改進程式碼
那麼,自然而然,我們可以將兩個類中相同的部分,抽象出來放入一個父類中,然後不同的部分讓子類去實現。
因此,我們可以編寫出父類,如下:
abstract class Beverage {
protected boolean isSweet;
// 榨果汁
public abstract void squeezeJuice();
// 將果汁倒入杯中
public abstract void juiceToCup();
// 向杯中倒入水
public void waterToCup() {
System.out.println("pour water into the cup");
}
// 向杯中倒入白糖
public void sugarToCup() {
System.out.println("pour sugar into the cup");
}
// 製作蘋果飲料
public final void makeBeverage() {
squeezeJuice();
juiceToCup();
waterToCup();
// 根據喜好是否加白糖
if (isSweet) {
sugarToCup();
}
}
}
我們將所有相同的程式碼都抽取到了 Beverage
類中,相同的部分有:
isSweet
變數waterToCup
方法sugarToCup
方法makeBeverage
方法
其中 makeBeverage
方法使用了 final
關鍵字來修飾,表示我們不希望子類去修改它。
不同的部分有:
squeezeJuice
方法juiceToCup
方法
這兩個方法都是抽象方法,表示我們希望子類根據自己的需求去實現。最終在子類中呼叫 makeBeverage
方法時,makeBeverage
會依據多型性來呼叫正確的 squeezeJuice
和 juiceToCup
方法。
下面編寫 AppleBeverage 類:
class AppleBeverage extends Beverage {
public AppleBeverage(boolean isSweet) {
this.isSweet = isSweet;
}
public void squeezeJuice() {
System.out.println("squeeze apple juice");
}
public void juiceToCup() {
System.out.println("pour the apple juice into the cup");
}
}
AppleBeverage 繼承了 Beverage,並且實現了 squeezeJuice
和 juiceToCup
方法。
再編寫 OrangeBeverage 類:
class OrangeBeverage extends Beverage {
public OrangeBeverage(boolean isSweet) {
this.isSweet = isSweet;
}
public void squeezeJuice() {
System.out.println("squeeze orange juice");
}
public void juiceToCup() {
System.out.println("pour the orange juice into the cup");
}
}
OrangeBeverage 繼承了 Beverage,並且實現了 squeezeJuice
和 juiceToCup
方法。
經過改進後的程式碼類圖如下:
可以看到經過改進的程式碼,重複的程式碼都抽取到了父類中,能複用的程式碼都進行了複用,子類只需根據自己的需要實現父類的抽象方法就行。
我將所有程式碼放在了這裡,供大家參考。
5,模板方法
實際上,上面程式碼的實現方式就使用到了模板方法模式。
模板方法模式在一個方法中定義一個演算法的骨架,而將一些步驟延遲到子類中,使得子類可以在不改變演算法結構的情況下,重新定義演算法中的某些步驟。
這裡的演算法指的是實際專案中的業務邏輯。
模板方法的類圖很簡單,如下:
模板方法模式的重點在於,它在父類中定義了一個通用的演算法框架(templateMethod),也就是上面程式碼中的 makeBeverage
方法,這個方法就是模板方法,一般用 final
修飾,用於防止子類覆蓋。
另外還有一些抽象方法,這些抽象方法都用 abstract
進行了修飾,表示必須由子類實現。演算法框架呼叫了這些抽象方法,這樣就相當於子類重新定義了演算法中的某些步驟。
在上面程式碼的 makeBeverage
方法中還用到了一個變數 isSweet
,這個變數在子類物件中的不同取值,會影響到 makeBeverage
的執行流程。
這個 isSweet
變數叫作“鉤子”,鉤子可以是一個變數,也可以是一個方法,它可以改變模板方法的執行流程。
6,總結
模板方法模式提供了一個演算法步驟,從中我們能看到程式碼複用的技巧。
模板方法模式中的抽象方法由子類實現,這意味著父類定義了演算法的框架流程,而將演算法的實現延遲到了子類中。
我們通常會將模板方法與工廠方法放在一起比較,這兩個模式有一個明顯的不同點,就是模板方法模式將一個演算法流程中的某些步驟的具體實現延遲到了子類中,而工廠方法模式是將物件的建立延遲到了子類中。
在 Java JDK 中,我們能看到很多模板方法的應用案例,比如 InputStream.read(byte b[], int off, int len)
方法。
(本節完。)
推薦閱讀:
歡迎關注作者公眾號,獲取更多技術乾貨。