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 中的重構是類似的,可以自己試試,現在應該可以更清楚地看出,print
與 nextInt
真正的行為實現推給子類別實作了,從原則的角度來看,出發點則是相依反轉,也就是 GuessGame
相依在抽象,而不是具體實現。
Servlet API 實例
Jakarta EE 的 Servlet API,HttpServlet
的實作就是 Template Method 的具體代表,如果你看看 HttpServlet
的 service
方法,其實流程大致如下:
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
// 略 ...
}
當請求來到時,容器會呼叫 Servlet
的 service
方法,在判斷 HTTP 請求的方式後,再分別呼叫 doGet
、doPost
等方法,這是因為不同的 Servlet,針對不同的 HTTP 請求,會有不同的目的,因此若想針對 GET
、POST
等方法進行處理,才會在繼承 HttpServlet
以後,重新定義相對應的 doGet
、doPost
方法。
別太著重於模式,就算你以某個模式開始思考,那也只是個思考的方向,不是最後的目的,不要想著要將程式重構成什麼模式,在思考的過程中,要不斷地調整,就算最後成果不是你一開始參考的模式,或者長得不像你參考的文件上某個範例、類別圖、架構等,也無需在意。
就算是 SOLID 原則,從不同角度來看,也可能是不同原則的實現,先前文件沒提到某個原則,並不代表該實作沒有思考該原則,只是行文上沒有特別提及罷了,畢竟每篇文件,都要 SOLID 原則全部談一遍,太麻煩且囉嗦啦!
強烈的約束性
父類別規範主要流程,真正的行為實現推給子類別實作,是透過 Template Method 思考時的重點,另外還有個必須深思的點,Template Method 是繼承了父類別實作的流程,繼承具有強烈的約束性,具體來說就是有 is a 的關係!
既然擁有強烈的約束性,那麼使用 Template Method 的場合,其實是需要再三思量的,什麼樣的場合比較適用呢?你想主導大部份的流程,然而又想開放一點自訂的彈性。
例如,你的物件有一套生命週期,然而在這個週期當中,可以讓使用者能安插一些自訂流程或註冊一些自訂服務,也就是你想要使用者能實現 hook 或 plugin;又或者你想實現某種框架,其中實作了某些通用的商務流程,然而細節部份,可以讓使用者自訂,方才的猜數字遊戲是過份簡單了些,然而 Servlet API 的例子就貼近這個概念了,畢竟 HTTP 請求的處理流程有其複雜性,HttpServlet
實作了通用的部份,你只要自訂 doGet
、doPost
等來完成需求就可以了。
當然,並不是 hook、plugin 或框架,就非得使用 Template Method 的概念,還是要需求與實際需要的彈性而定,模式終究只是個方向,不會是最後的成果。