模板方法模式-封裝一套演算法流程

公號:碼農充電站pro
主頁://codeshellme.github.io

今天來介紹模板方法模式Template Method Design Pattern)。

1,製作飲料的過程

在這裡插入圖片描述

假如我們要製作兩種飲料:蘋果飲料和橙子飲料。這兩種飲料的製作流程如下:

  • 蘋果飲料製作流程
    • 把蘋果榨成蘋果汁
    • 將蘋果汁倒入杯中
    • 向杯中倒入水
    • 根據客戶喜好是否放入白糖
      • 喜歡則放入白糖,否則不放白糖
  • 橙子飲料製作流程
    • 把橙子榨成橙汁
    • 將橙汁倒入杯中
    • 向杯中倒入水
    • 根據客戶喜好是否放入白糖
      • 喜歡則放入白糖,否則不放白糖

2,模擬製作飲料

如果要模擬飲料的製作過程,按照最直接的想法,我們創建兩個類 AppleBeverageOrangeBeverage 分別用於製作蘋果飲料和橙子飲料。

首先根據蘋果飲料的製作流程編寫 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,分析程式碼

可以看到上面兩個類的程式碼非常簡單,為了更加詳細的分析,我畫出了這兩個類的類圖:

在這裡插入圖片描述

我將這兩個類中的方法相同的部分用藍色標了出來,可以看到,這兩個類中的 waterToCupsugarToCup 方法一模一樣,其它三個方法也是非常的相似。

這樣的程式碼顯然是沒有復用已有的程式碼。

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 會依據多態性來調用正確的 squeezeJuicejuiceToCup 方法。

下面編寫 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,並且實現了 squeezeJuicejuiceToCup 方法。

再編寫 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,並且實現了 squeezeJuicejuiceToCup 方法。

經過改進後的程式碼類圖如下:

在這裡插入圖片描述

可以看到經過改進的程式碼,重複的程式碼都抽取到了父類中,能復用的程式碼都進行了復用,子類只需根據自己的需要實現父類的抽象方法就行。

我將所有程式碼放在了這裡,供大家參考。

5,模板方法

實際上,上面程式碼的實現方式就使用到了模板方法模式

模板方法模式在一個方法中定義一個演算法的骨架,而將一些步驟延遲到子類中,使得子類可以在不改變演算法結構的情況下,重新定義演算法中的某些步驟

這裡的演算法指的是實際項目中的業務邏輯

模板方法的類圖很簡單,如下:

在這裡插入圖片描述

模板方法模式的重點在於,它在父類中定義了一個通用的演算法框架templateMethod),也就是上面程式碼中的 makeBeverage 方法,這個方法就是模板方法,一般用 final 修飾,用於防止子類覆蓋。

另外還有一些抽象方法,這些抽象方法都用 abstract 進行了修飾,表示必須由子類實現。演算法框架調用了這些抽象方法,這樣就相當於子類重新定義了演算法中的某些步驟。

在上面程式碼的 makeBeverage 方法中還用到了一個變數 isSweet,這個變數在子類對象中的不同取值,會影響到 makeBeverage 的執行流程。

這個 isSweet 變數叫作「鉤子」,鉤子可以是一個變數,也可以是一個方法,它可以改變模板方法的執行流程。

6,總結

模板方法模式提供了一個演算法步驟,從中我們能看到程式碼復用的技巧。

模板方法模式中的抽象方法由子類實現,這意味著父類定義了演算法的框架流程,而將演算法的實現延遲到了子類中。

我們通常會將模板方法工廠方法放在一起比較,這兩個模式有一個明顯的不同點,就是模板方法模式將一個演算法流程中的某些步驟的具體實現延遲到了子類中,而工廠方法模式是將對象的創建延遲到了子類中。

Java JDK 中,我們能看到很多模板方法的應用案例,比如 InputStream.read(byte b[], int off, int len) 方法。

(本節完。)


推薦閱讀:

觀察者模式-將消息通知給觀察者

裝飾者模式-動態的包裝原有對象的行為

命令模式-將請求封裝成對象

適配器模式-讓不兼容的介面得以適配

外觀模式-簡化子系統的複雜性


歡迎關注作者公眾號,獲取更多技術乾貨。

碼農充電站pro