Factory Method

December 21, 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夢喔!我又沒有時光機….」

相依在實作

乍看這需求是不合理,不過仔細想想,問題在於輸入輸出尚未決定,然而目前的程式相依在實際的輸出輸出,也就是標準輸入輸出,能不能先重構一下,將標準輸入輸出的部份拉出來呢?

import java.util.Scanner;

class ConsoleIO {
	Scanner console = new Scanner(System.in);
	
	public int nextInt() {
		return console.nextInt();
	}
	
	public void print(String text) {
		System.out.print(text);
	}
}

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

現在 Guess 相依在 ConsoleIO,更具體地說,行為上相依在 ConsoleIOnextIntprint 實作,為了能不相依具體實作,來定義 IO 的行為:

import java.util.Scanner;

interface IO {
	int nextInt();
	void print(String text);
}

class ConsoleIO implements IO {
	Scanner console = new Scanner(System.in);
	
	public int nextInt() {
		return console.nextInt();
	}
	
	public void print(String text) {
		System.out.print(text);
	}
}

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

相依在抽象

接著將 Guess 的遊戲流程抽出來,這個流程中基本上不確定的部份是輸入輸出,為了不相依在具體的部份,取得 IO 實例的部份定義為抽象方法:

abstract class GuessGame {
    public void go() {
    	var 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();
	}
}

GuessGame 現在相依在抽象的 IO 了,若想有個文字模式猜數字,可以這麼撰寫:

public class Guess {
    public static void main(String[] args) {
        var game = new ConsoleGame();
        game.go();
    }
}

如果要有其他環境的猜數字遊戲呢?那就實作 GuessGameinputOutput,傳回實作了 IO 介面的實例就可以了,GuessGameinputOutput 就像個黑箱,或說是個工廠,視你需要的環境而定,其中可能封裝了複雜的流程或是生命週期管理。

像這樣,將原本相依在具體的類別(ConsoleIO),改為相依在抽象的介面(IO),真正的生成實現,推給子類別實作 inputOutput 方法時決定,有時在實現程式庫或框架時,不知不覺就會發現自己在做類似的設計,這種相似性構成的模式就給它一個 Factory Method 的名稱。

相依反轉

不用太去計較 Factory Method 的實現會是長什麼樣子,一些書或文件可能會用不同的方式來介紹這個模式,不同的需求下,實現方式本來也會有所調整,模式這種東西,過於執著於實現樣貌,會變成本未倒置的行為。

別的不說,這個猜數字遊戲的案例,從 IO 的建立來看,也就是從分離物件生成與使用的角度來看,是 Factory Method,然而從物件間的交流行為上來看,可以歸為 Template Method 喔!

真正該留意的是,這種將原本相依在具體,改為相依在抽象的原則,稱為 Dependency inversion,inversion 的意思是指,高層模組不相依在具體的底層模組實作,應該是反過來,低層模組要依據高層模組規範的抽象來實現,當你需要為未來預留一些彈性時,相依反轉總是一個好的思考方向,Factory Method 不過就是其中一個模式罷了。