在很多編輯軟體中都有複製
這個功能,例如 Google Slides 的複製投影片,或是 PicCollage 的複製文字。這些情況就很理所當然的使用了 copy
或 clone
。但更進一步的是,通常複製了之後,是想要用這些已經做好的原型 (prototype
) 來做一些修改,例如更換文字顏色,或是改變字體。
這就是 Prototype Pattern,非常適合用在有深層結構而且高度自定義的物件上面,在這種情況下你通常不會想從頭建立這些物件,另一方面也可以提高程式碼的可讀性。
上面的這種情況是由使用者來做複製並修改,但還有另外一種情況是應用程式本身需要生成一系列類似的物件,這時候應用程式會自行複製並修改,而不需要經過使用者,這正好就是 GoF 這本書所舉的範例。
Prototype 會有一個根節點的物件,以及這個根節點物件的所有屬性物件,這些所有的物件都需要去實作 copy
,通常我們稱這為 deep copy
。
舉例來說我現在有一個 TextModel
,他是根節點物件,裡面有一些屬性像是 font
, text
, alignment
, color
, background
, style
。而 Font 也有可能有一些屬性像是 typeFace
, fontName
。在實作 Prototype 時,textModel
, font
, text
, alignment
, color
, background
, style
, typeFace
, fontName
都要能夠 copy。如果遇到 primitive type (int, long, byte, boolean) 的時候就不需要實作 copy()
,直接 assign 就好,需要 copy()
是為了避免使用同一個 reference。
這邊用 Java 來當例子:
interface Prototype<T> {
T copy()
}
class TextModel implements Prototype<TextModel> {
//java 通常會有 getters 跟 setters,這邊為了節省篇幅不這樣做了
public Font font;
public String text;
public String alignment;
int color;
int background;
String style;
public TextModel copy() {
//需要 copy font,因為他不是 primitive type,要確保不會共享變數
return new TextModel(font.copy(), text, alignment, color, background, style);
}
}
class Font implements Prototype<Font>{
public Typeface typeface;
public String fontName;
public Font copy() {
//不只是根物件需要呼叫 copy()
return new Font(typeface.copy(), fontName);
}
}
class Typeface implements Prototype<Typeface>{
public String fontFamily;
public String sourceUrl;
public Typeface copy() {
//不需要再呼叫任何 copy() 了,兩個成員變數都是 primitive type
return new Typeface(fontFamily, fontName);
}
}
另外值得一提的是這個 pattern 通常只會用在** data object** 上面(或稱作 Entity, Value Object),這也是一個檢視設計的很好時機。如果這個 data object 有太多職責,例如做 DB 的 CRUD 操作、或是有一些商業邏輯相關的狀態。在這種情況下會很難實作 copy 。
以下就是個不好的範例:
class TextModel {
public Font font;
public String text;
...
// 要連 dbHelper 一起 copy 嗎?不!請將 DBHelper 移走
private DBHelper dbHelper;
public void insertTextModel(TextModel textModel) {
dbHelper.insert(textModel);
}
}
copy()
,尤其是子類別有 override 父類別行為的時候,這時候會因為不同的呼叫時機點而產生難以控制的行為(這裡說的父類別可能是具體類別或是抽象類別,但不包含 interface)。Effective Java 第 17 條這樣說:Design and document for inheritance or else prohibit it.
通常這時候你應該要考慮的不是如何安全的實作 copy()
,而是考慮使用組合來取代繼承。如果真的需要在繼承的類別實作 copy()
怎麼辦呢?將實作放在子類別吧,抽象類別可以選擇不實作。
要注意這邊指的 Static Factory method 不是 GoF 裡的 Factory method. Static Factory method 是用來針對特定情境來建立實例,而且不綁定在當下的實例。例如建立預設的 TextModel,可能顏色是黑色、字體是 Roboto、背景透明,當每次需要拿預設的 TextModel 時,都可以使用這個 method。
下面是一個 Static Factory method 的例子
class TextModel {
...
public static TextModel default() {
return new TextModel(... , Color.Black, Color.Transparent, ...)
}
}
Prototype 則是已經建立好一個物件了,想建立另一個類似物件時不想要再經歷一次建立物件的流程,直接從原有的實例來複製是相對快速的做法。
但相同的是他們兩個 pattern 都可以封裝建立實例的細節,不管建立一個物件需要多少 dependency 都不需要知道。可以大大的增加可讀性。
public void openTextEditorByCopy() {
TextModel lastTextModel = getLastTextModel();
//Client 不需要知道這些細節!!
TextModel newTextModel = new TextModel(
new Font(
new TypeFace(lastTextModel.font.typeface.fontFamily,
lastTextModel.font.typeface.sourceUrl),
lastTextModel.font.fontName
)
...
);
view.openTextEditor(newTextModel);
}
public void openTextEditorByCopy() {
TextModel lastTextModel = getLastTextModel();
//簡單好讀
TextModel copiedTextModel = lastTextModel.copy();
view.openTextEditor(copiedTextModel);
}
public void openTextEditorByNewText() {
//可以依據使用情境來決定要使用哪一個 pattern
TextModel newTextModel = TextModel.default();
view.openTextEditor(newTextModel);
}
如果下次發現浪費了很多時間在建立物件上,而且做了很多次一模一樣的 IO 的話,別忘了 Prototype Pattern ,它很有可能正好解決你的問題!另一方面也可以想想你遇過哪些類似的情境,像是上面的 Static Facatory method。嘗試分析他們之間的差異,我相信一定會獲得不少收穫的!今天的介紹就到這邊,感謝你的閱讀!
作者:Yanbin