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
規範了迷宮建造各區塊時可用的方法,Maze
的 render
規範了建造迷宮的流程,過程中使用 MazeBuilder
實例建造各區塊,在最後透過 build
取得迷宮的表現,送至標準輸出;如果想要其他的迷宮表現,例如用全形字元來表示各區塊,就實作 MazeBuilder
並指定給 render
方法。
就模式名稱而言,這是 Gof 書中的 Builder 模式,歸類在創建模式之中,因為指示 builder 打造各部位後,最後才創建了表現物件,通常最後的表現物件會是不可變動(immutable),畢竟建造過程你已經指示了表現物件,最終應該處於哪個狀態。
StringBuilder?
方才的範例中,我特別使用了 StringBuffer
,如果你熟悉 Java,可能會想為什麼不使用 StringBuilder
?嗯?為什麼 StringBuilder
名稱會有 builder 字樣?
這就是我一開始特別不使用 StringBuilder
的原因,StringBuilder
本身就是 Builder 模式中的 builder 角色,可以依你的指示建立最後的字串,實際上 StringBuffer
也實現了 builder 角色,畢竟 StringBuilder
、StringBuffer
在 API 上是一致的,append
等方法就是建立字串各部位的方法,toString
就是最後取得字串表現的方法。
一開始特別不使用 StringBuilder
是避免混淆想封裝的對象,對文字模式迷宮而言,最後的表現雖然使用了字串,實際上想封裝的對象是使用哪個符號代表哪個類型的區塊。
那麼就原則而言,這個模式實現了哪個呢?喔!這沒有標準答案,原則是用來思考的,也是必須依需求來判定的,例如,這邊談到了分離流程/表現,流程與表現都是關心的對象,要將之分離,是因為什麼呢?今天與明天你實作的東西不同?朋友或同事跟你合作,而你們各自實作流程、表現?你現在關心什麼?一個月後關心什麼?你在意什麼?合作夥伴又負責什麼呢?
這一連串問句,是從關注分離的原則來看,之前的文件中還談過其他原則,該怎麼思考?是否符合原則?那是你自己要依情境判定的!
話說,為什麼這邊要特別在標題上強調 Gof 呢?雖說模式的名稱是用來溝通的,不過程式設計領域,名詞往往是沒有標準定義的,很容易有重疊,很容易模擬兩可,你可能也看過 API 中一些以 Builder 為名,然而跟這邊看的角度不同,因而會有「你的 Builder 不是我的 Builder」 的狐疑。
這很正常啊!Gof 談過 Builder,《Effective Java》也談過 Builder,兩者都叫 Builder,概念上有一些重疊,然而關注的點卻又不一樣!這就在〈Builder(Effective Java)〉中再聊了…