Builder(Gof)

December 23, 2021

我喜歡用程式碼來從事 3D 建模,由於程式碼本身易於描述規律,具有規律性的迷宮建造,自然就是創作的靈感來源之一,我搭建過多種不同類型的迷宮

文字模式迷宮

如果你對迷宮的規律有興趣,可以參考〈玩轉 p5.js〉中有關迷宮的介紹,這邊呢…就用迷宮來作為案例好了,如果你想建個簡單的文字模式迷宮:

package cc.openhome;

public class Main {
	final static int R = 0; // 通道
	final static int O = 1; // 障礙
	final static int T = 2; // 寶物

    public static void main(String[] args) {
		... 一些迷宮演算法產生 cells 資料

        var cells = new int[][] {
            {O, O, O, O, O, O, O}, 
            {O, R, R, R, R, T, O}, 
            {O, R, O, R, O, R, O}, 
            {O, R, T, O, R, O, O}, 
            {O, O, R, O, R, O, O}, 
            {O, R, R, T, R, R, O}, 
            {O, O, O, O, O, O, O}
        };
        
        for(var row : cells) {
            for(var cell : row) {
            	switch(cell) {
            	case R:
            		System.out.print(' ');
            		break;
            	case O:
            		System.out.print('x');
            		break;
            	case T:
               		System.out.print('$');
            		break;
            	}
            }
            System.out.println();
        }
    }
}

上面的範例中,用了 x 等符號來表示迷宮中每個區塊,如果今天你想改用其他符號呢?修改 Main?如果你想建立更多不同外觀的迷宮呢?繼續修改 Main

分離流程/表現

我能有許多迷宮作品的原因在於,迷宮的建構流程往往是類似的,只是外觀表現上不同,若想要分離流程與表現,首先得辨識最後想要取得的表現是什麼,因此先來重構一下:

public class Main {
	final static int R = 0; // 通道
	final static int O = 1; // 障礙
	final static int T = 2; // 寶物

    public static void main(String[] args) {
		... 一些迷宮演算法產生 cells 資料

        var cells = new int[][] {
            {O, O, O, O, O, O, O}, 
            {O, R, R, R, R, T, O}, 
            {O, R, O, R, O, R, O}, 
            {O, R, T, O, R, O, O}, 
            {O, O, R, O, R, O, O}, 
            {O, R, R, T, R, R, O}, 
            {O, O, O, O, O, O, O}
        };
        
        var buffer = new StringBuffer();
        for(var row : cells) {
            for(var cell : row) {
            	switch(cell) {
            	case R:
            		buffer.append(' ');
            		break;
            	case O:
            		buffer.append('x');
            		break;
            	case T:
            		buffer.append('$');
            		break;
            	}
            }
            buffer.append('\n');
        }
        
        System.out.println(buffer.toString());
    }
}

因為這是文字模式迷宮,最後的迷宮表現是要送到標準輸出,也就是說,字串就是最後的迷宮表現,接著,為了能替換建立表現的物件,來進一步重構:

interface MazeBuilder {
	void road();
	void obstacle();
	void treasure();
	void nextRow();
	String build();
}

class SimpleMazeBuilder implements MazeBuilder {
	StringBuffer buffer = new StringBuffer();
	
	@Override
	public void road() {
		buffer.append(' ');
	}

	@Override
	public void obstacle() {
		buffer.append('x');
	}

	@Override
	public void treasure() {
		buffer.append('$');
	}

	@Override
	public void nextRow() {
		buffer.append('\n');
	}

	@Override
	public String build() {
		return buffer.toString();
	}
}

class Maze {
	final static int R = 0; // 通道
	final static int O = 1; // 障礙
	final static int T = 2; // 寶物
	
	private int[][] cells;
	
	Maze generate() {
		... 一些迷宮演算法產生 cells 資料

		cells = new int[][] {
            {O, O, O, O, O, O, O}, 
            {O, R, R, R, R, T, O}, 
            {O, R, O, R, O, R, O}, 
            {O, R, T, O, R, O, O}, 
            {O, O, R, O, R, O, O}, 
            {O, R, R, T, R, R, O}, 
            {O, O, O, O, O, O, O}
        };
        return this;
	}
	
	void render(MazeBuilder builder) {
        for(var row : cells) {
            for(var cell : row) {
            	switch(cell) {
            	case R:
            		builder.road();
            		break;
            	case O:
            	    builder.obstacle();
            		break;
            	case T:
            		builder.treasure();
            		break;
            	}
            }
            builder.nextRow();
        }
        System.out.println(builder.build());
	}
}

public class Main {
    public static void main(String[] args) {
        var maze = new Maze();
        maze.generate()
            .render(new SimpleMazeBuilder());
    }
}

MazeBuilder 規範了迷宮建造各區塊時可用的方法,Mazerender 規範了建造迷宮的流程,過程中使用 MazeBuilder 實例建造各區塊,在最後透過 build 取得迷宮的表現,送至標準輸出;如果想要其他的迷宮表現,例如用全形字元來表示各區塊,就實作 MazeBuilder 並指定給 render 方法。

就模式名稱而言,這是 Gof 書中的 Builder 模式,歸類在創建模式之中,因為指示 builder 打造各部位後,最後才創建了表現物件,通常最後的表現物件會是不可變動(immutable),畢竟建造過程你已經指示了表現物件,最終應該處於哪個狀態。

StringBuilder?

方才的範例中,我特別使用了 StringBuffer,如果你熟悉 Java,可能會想為什麼不使用 StringBuilder?嗯?為什麼 StringBuilder 名稱會有 builder 字樣?

這就是我一開始特別不使用 StringBuilder 的原因,StringBuilder 本身就是 Builder 模式中的 builder 角色,可以依你的指示建立最後的字串,實際上 StringBuffer 也實現了 builder 角色,畢竟 StringBuilderStringBuffer 在 API 上是一致的,append 等方法就是建立字串各部位的方法,toString 就是最後取得字串表現的方法。

一開始特別不使用 StringBuilder 是避免混淆想封裝的對象,對文字模式迷宮而言,最後的表現雖然使用了字串,實際上想封裝的對象是使用哪個符號代表哪個類型的區塊。

那麼就原則而言,這個模式實現了哪個呢?喔!這沒有標準答案,原則是用來思考的,也是必須依需求來判定的,例如,這邊談到了分離流程/表現,流程與表現都是關心的對象,要將之分離,是因為什麼呢?今天與明天你實作的東西不同?朋友或同事跟你合作,而你們各自實作流程、表現?你現在關心什麼?一個月後關心什麼?你在意什麼?合作夥伴又負責什麼呢?

這一連串問句,是從關注分離的原則來看,之前的文件中還談過其他原則,該怎麼思考?是否符合原則?那是你自己要依情境判定的!

話說,為什麼這邊要特別在標題上強調 Gof 呢?雖說模式的名稱是用來溝通的,不過程式設計領域,名詞往往是沒有標準定義的,很容易有重疊,很容易模擬兩可,你可能也看過 API 中一些以 Builder 為名,然而跟這邊看的角度不同,因而會有「你的 Builder 不是我的 Builder」 的狐疑。

這很正常啊!Gof 談過 Builder,《Effective Java》也談過 Builder,兩者都叫 Builder,概念上有一些重疊,然而關注的點卻又不一樣!這就在〈Builder(Effective Java)〉中再聊了…