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
,更具體地說,行為上相依在 ConsoleIO
的 nextInt
與 print
實作,為了能不相依具體實作,來定義 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();
}
}
如果要有其他環境的猜數字遊戲呢?那就實作 GuessGame
的 inputOutput
,傳回實作了 IO
介面的實例就可以了,GuessGame
的 inputOutput
就像個黑箱,或說是個工廠,視你需要的環境而定,其中可能封裝了複雜的流程或是生命週期管理。
像這樣,將原本相依在具體的類別(ConsoleIO
),改為相依在抽象的介面(IO
),真正的生成實現,推給子類別實作 inputOutput
方法時決定,有時在實現程式庫或框架時,不知不覺就會發現自己在做類似的設計,這種相似性構成的模式就給它一個 Factory Method 的名稱。
相依反轉
不用太去計較 Factory Method 的實現會是長什麼樣子,一些書或文件可能會用不同的方式來介紹這個模式,不同的需求下,實現方式本來也會有所調整,模式這種東西,過於執著於實現樣貌,會變成本未倒置的行為。
別的不說,這個猜數字遊戲的案例,從 IO
的建立來看,也就是從分離物件生成與使用的角度來看,是 Factory Method,然而從物件間的交流行為上來看,可以歸為 Template Method 喔!
真正該留意的是,這種將原本相依在具體,改為相依在抽象的原則,稱為 Dependency inversion,inversion 的意思是指,高層模組不相依在具體的底層模組實作,應該是反過來,低層模組要依據高層模組規範的抽象來實現,當你需要為未來預留一些彈性時,相依反轉總是一個好的思考方向,Factory Method 不過就是其中一個模式罷了。