Prototype
December 26, 2021如果你的物件狀態是可變動(mutable), 若想將之作為引數傳遞,有時會顧慮到一個問題,你不希望該物件被函式改變,解決的方式之一,就是對物件進行複製,用複製品作為引數,函式要真改變了引數的狀態,對來源物件也不會有影響。
將一個物件作為來源進行複製,基本上是沒什麼大不了的問題,就取得來源物件的重要狀態,建立一個新物件啊!問題就在於「建立」這個動作誰來做,有時會是個考量點!
王氏磚
例如,來玩玩〈王氏磚〉,從文件中可以看到,基本的 2-edge 王氏磚會有 16 種類型,拼接時只是依照網格資料,建立對應類型的王氏磚並設定至相應的位置。
因為使用的王氏磚只有那 16 種類型,你可能會想建立一組王氏磚,設定好外觀後作為原型,這麼一來需要某塊磚時,只要複製原型的外觀資訊,並修改相應位置資訊就可以了。
class Tile01 {
private int x;
private int y;
Tile01(一些建構資訊) {}
void pos(int x, int y) {
this.x = x;
this.y = y;
}
...
}
class Tile02 {
private int x;
private int y;
Tile02(一些建構資訊) {}
void pos(int x, int y) {
this.x = x;
this.y = y;
}
...
}
class WangTiles {
Object[] prototypes = new Object[16];
// 註冊原型
void tile(int type, Object tile) {
prototypes[type] = tile;
}
void generate() {
...隨機產生 grids 代表王氏磚網格資訊
for(Grid grid : grids) {
// 取得對應原型
var prototype = prototypes[grid.type];
switch(grid.type) {
case 0:
var tile01 = (Tile01) prototype;
var tile = new Tile01(從tile01取得建構資訊);
tile.pos(grid.x, grid.y);
...繪圖或其他動作
break;
case 1:
var tile02 = (Tile02) prototype;
var tile = new Tile02(從tile02取得建構資訊);
tile.pos(grid.x, grid.y);
...繪圖或其他動作
break;
case 2:
...其他類型王氏磚的原型處理到 case 15...
}
}
}
}
這樣的設計基本上也不是不行,不過呢!其實不只有 2-edge 王氏磚,如果你想設計一個更通用的 WangTiles
,那麼以上逐一比對類型資訊、採用對應的建構的方式,顯然行不通;就算你只想針對 2-edge 王氏磚,萬一你設計其他風格的拼接塊,有不同的建構式,WangTiles
勢必得做出修改。
設計的原則
就方才的描述而言,需求變更,程式碼就得做出修改,顯然不符合開放關閉原則;另一方面,WangTiles
負責了不屬於它的職責,畢竟最知道該怎麼複製物件的,應該是物件本身,由 WangTiles
來處理物件複製,不符合單一職責(single responsibility)原則。
既然拼接塊本身要負責如何複製自身,那麼拼接塊的行為介面上,應該要規範複製的行為,例如,定義一個 Tile
類別,有個 copy
方法?
abstract class Tile {
private int x;
private int y;
void pos(int x, int y) {
this.x = x;
this.y = y;
}
abstract Tile copy();
}
因為拼接塊的行為之一,是要能設定位置,以上的類別也就一定規範了,你可能會想到,Java 本身的話,Object
就定義了每個物件都會有個 clone
行為,是沒錯!如果你本身對 Java 的 clone
有足夠的認識,要直接利用也是可以的,只是這邊先假設 Java 沒這機制,讓範例單純一些,畢竟使用 Java 的 clone
機制,有其必須遵守的規範(可參考 API 文件)。
總之,有了以上的 Tile
,來重構一下範例:
class Tile01 extends Tile {
Tile01(一些建構資訊) {}
@Override
Tile copy() {
return new Tile01(從自身取得建構資訊);
}
}
class Tile02 {
Tile02(一些建構資訊) {}
Tile copy() {
return new Tile02(從自身取得建構資訊);
}
}
class WangTiles {
Tile[] prototypes = new Tile[16];
void tile(int type, Tile tile) {
prototypes[type] = tile;
}
void generate() {
...隨機產生 grids 代表王氏磚網格資訊
for(Grid grid : grids) {
var tile = prototypes[grid.type].copy();
tile.pos(grid.x, grid.y);
...繪圖或其他動作
}
}
}
這樣感覺好多了呢!而且,在更複雜的情境下,例如作為原型的物件,可能內含其他物件,這些物件在複製時,要不要參與複製?必須淺層或深層複製?這些問題只有物件本身自己清楚,如果各物件各自實現了自己的 copy
,那麼需要複製時就只要呼叫各物件的 copy
,也能實現關切分離的概念。
JavaScript 原型鏈
像方才的範例,是用來處理一組原型的複製問題,Gof 稱這個模式為 Prototype 模式。
有的語言本身會有些支援物件複製的機制,例如 Java 的 clone
、Python 的 copy
模組,考量使用這類特性時,會讓 Prototype 模式在實作上略有不同。
談到 Prototype,JavaScript 不是有原型鏈嗎?跟 Prototype 模式有什麼關係嗎?嗯…只是剛好都叫 prototype 啦!不過,如果配合 Object.create
,加上 JavaScript 是動態定型,又支援物件個體化,實作 Prototype 模式有時是會簡單一些:
class WangTiles {
constructor() {
this.prototypes = [];
}
tile(type, tile) {
prototypes[type] = tile;
}
generate() {
const grids = [];
grids.forEach(grid => {
const tile = Object.create(prototypes[grid.type])
tile.x = grid.x;
tile.y = grid.y;
...繪圖或其他動作
});
}
}
Object.create
會建立新物件,並以指物件被建立的新物件之 prototype
特性,如果你存取的特性不在傳回的新物件上,就會去 prototype
上找,如果存取 x
、y
特性,就會使用新物件的 x
、y
特性。
這個例子中,並不是將複製行為定義在物件上,而是透過 Object.create
,要不要這麼用還是看需求,這只是用來說明語言特性如何實現 Prototype 模式的一個例子,模式就只是一個思考的方向,別死死板板地套用,也不是一定就得怎麼用,或者實作後應該長什麼樣!