Template Method

December 22, 2021

撰寫程式常有些看似不合理但又非得完成的需求,舉例來說,老闆叫你開發一個猜數字遊戲,要隨機產生 0 到 9 的數字,使用者輸入的數字如果相同就顯示「猜中了」,如果不同就繼續讓使用者輸入數字,直到猜中為止。

最初的設計

這程式有什麼難的?

import java.util.Scanner;

public class Guess {
    public static void main(String[] args) {
        var console = new Scanner(System.in);
        var number = (int) (Math.random() * 10); 
        var guess = -1;
        do {
            System.out.print("輸入數字:");
            guess = console.nextInt();
        } while(guess != number);
        System.out.println("猜中了");
    }
}

圓滿達成任務是吧!將程式交給老闆後,老闆皺著眉頭說:「我有說要在文字模式下執行遊戲嗎?」你就問了:「請問會在哪個環境執行呢?」老闆:「還沒決定,也許會用視窗程式,不過改成網頁也不錯,唔…下個星期開會討論一下。」你問:「那可以下星期討論完我再來寫嗎?」老闆:「不行!」你(內心 OS):「當我是哆啦A夢喔!我又沒有時光機….」

相依在抽象

唉?等等!方才這段描述怎麼似曾相似?在哪邊看過呢?Factory Method?沒錯!來看看 Factory Method 重構後的範例:

abstract class GuessGame {
    public void go() {
    	IO io = inputOutput();
        var number = (int) (Math.random() * 10); 
        var guess = -1;
        do {
        	io.print("輸入數字:");
            guess = io.nextInt();
        } while(guess != number);
        io.print("猜中了");
    }
    
    public abstract IO inputOutput();
}

class ConsoleGame extends GuessGame {
	@Override
	public IO inputOutput() {
		return new ConsoleIO();
	}
}

從分離物件生成與使用的角度來看,IO 真正的生成實現推給子類別實作,可以歸為 Factory Method;然而從物件間的交流行為上來看,inputOutput 真正的行為實現推給子類別實作,可以歸為 Template Method。

如果你太在意模式的表面形式,很容易會落入一個陷阱「結構上一定得長什麼樣,架構上一定要如何,才認為它是什麼模式…」這種想法不正確,開發時實際面對的需求錯綜複雜,從不同的角度來看,會需要不同的思考方向與解決方式,從而從不同的觀點來看實作結果,可能會發現有著各個不同似曾相似的設計,更具體而言,就是摻雜了各種模式。

如果你還是有點執著,那麼就來看這個吧!

abstract class GuessGame {
    public void go() {
        var number = (int) (Math.random() * 10); 
        var guess = -1;
        do {
            print("輸入數字:");
            guess = nextInt();
        } while(guess != number);
        println("猜中了");
    }
    
    public void println(String text) {
        print(text + "\n");
    }

    public abstract void print(String text);
    public abstract int nextInt();
}

public class ConsoleGame extends GuessGame {
    private Scanner console = new Scanner(System.in);
    
    @Override
    public void print(String text) {
        System.out.print(text);
        
    }

    @Override
    public int nextInt() {
        return console.nextInt();
    }
}

想透過重構,讓一開始的範例成為以上結果,思考的過程與 Factory Method 中的重構是類似的,可以自己試試,現在應該可以更清楚地看出,printnextInt 真正的行為實現推給子類別實作了,從原則的角度來看,出發點則是相依反轉,也就是 GuessGame 相依在抽象,而不是具體實現。

Servlet API 實例

Jakarta EE 的 Servlet API,HttpServlet 的實作就是 Template Method 的具體代表,如果你看看 HttpServletservice 方法,其實流程大致如下:

protected void service(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {
    String method = req.getMethod(); // 取得請求的方法
    if (method.equals(METHOD_GET)) { // HTTP GET
    // 略...
        doGet(req, resp);
        // 略 ...
    } else if (method.equals(METHOD_HEAD)) { // HTTP HEAD
        // 略 ...
        doHead(req, resp);
    } else if (method.equals(METHOD_POST)) { // HTTP POST
    // 略 ...
        doPost(req, resp);
    } else if (method.equals(METHOD_PUT)) { // HTTP PUT
    // 略 ...
}

當請求來到時,容器會呼叫 Servletservice 方法,在判斷 HTTP 請求的方式後,再分別呼叫 doGetdoPost 等方法,這是因為不同的 Servlet,針對不同的 HTTP 請求,會有不同的目的,因此若想針對 GETPOST 等方法進行處理,才會在繼承 HttpServlet 以後,重新定義相對應的 doGetdoPost 方法。

別太著重於模式,就算你以某個模式開始思考,那也只是個思考的方向,不是最後的目的,不要想著要將程式重構成什麼模式,在思考的過程中,要不斷地調整,就算最後成果不是你一開始參考的模式,或者長得不像你參考的文件上某個範例、類別圖、架構等,也無需在意。

就算是 SOLID 原則,從不同角度來看,也可能是不同原則的實現,先前文件沒提到某個原則,並不代表該實作沒有思考該原則,只是行文上沒有特別提及罷了,畢竟每篇文件,都要 SOLID 原則全部談一遍,太麻煩且囉嗦啦!

強烈的約束性

父類別規範主要流程,真正的行為實現推給子類別實作,是透過 Template Method 思考時的重點,另外還有個必須深思的點,Template Method 是繼承了父類別實作的流程,繼承具有強烈的約束性,具體來說就是有 is a 的關係!

既然擁有強烈的約束性,那麼使用 Template Method 的場合,其實是需要再三思量的,什麼樣的場合比較適用呢?你想主導大部份的流程,然而又想開放一點自訂的彈性。

例如,你的物件有一套生命週期,然而在這個週期當中,可以讓使用者能安插一些自訂流程或註冊一些自訂服務,也就是你想要使用者能實現 hook 或 plugin;又或者你想實現某種框架,其中實作了某些通用的商務流程,然而細節部份,可以讓使用者自訂,方才的猜數字遊戲是過份簡單了些,然而 Servlet API 的例子就貼近這個概念了,畢竟 HTTP 請求的處理流程有其複雜性,HttpServlet 實作了通用的部份,你只要自訂 doGetdoPost 等來完成需求就可以了。

當然,並不是 hook、plugin 或框架,就非得使用 Template Method 的概念,還是要需求與實際需要的彈性而定,模式終究只是個方向,不會是最後的成果。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter