iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 11
0

[Design Pattern] Prototype 原型模式

在很多編輯軟體中都有複製這個功能,例如 Google Slides 的複製投影片,或是 PicCollage 的複製文字。這些情況就很理所當然的使用了 copyclone。但更進一步的是,通常複製了之後,是想要用這些已經做好的原型 (prototype) 來做一些修改,例如更換文字顏色,或是改變字體。

這就是 Prototype Pattern,非常適合用在有深層結構而且高度自定義的物件上面,在這種情況下你通常不會想從頭建立這些物件,另一方面也可以提高程式碼的可讀性。

上面的這種情況是由使用者來做複製並修改,但還有另外一種情況是應用程式本身需要生成一系列類似的物件,這時候應用程式會自行複製並修改,而不需要經過使用者,這正好就是 GoF 這本書所舉的範例

Prototype Pattern 介紹

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。

Foo

Prototype 範例程式

這邊用 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);
   }
}

Prototype 使用時機 & 優缺點

  • 有些時候建立物件是非常耗時的,做了很多 IO 操作(DB, file, Api and parse json)才拿到所需要的物件。在需要建立類似物件時,會希望耗時操作越少越好,使用 Prototype 就可以減少這類型的消耗。
  • 多執行緒或跨類別操作時可以藉由 Prototype 來避免共享變數,共享變數會造成不可預期的資料修改或是 Deadlock 。雖然在原書中該 pattern 沒有限制物件要是 immutable 的,但是在這種情形下搭配 immutable 物件會很適合的。
  • 缺點是實作 copy 比較繁瑣而且無聊,有些 library 或是語言本身可以幫助你完成這件事,像是 Java 的 Lombok 可以藉由 annotation 自動程式碼,或是 kotlin 的 Arrow 可以生成 Lens (嚴格的來說這並不符合 Prototype Pattern 的定義,但是能夠獲得一樣的效果)。
  • 避免在繼承的類別上使用 copy(),尤其是子類別有 override 父類別行為的時候,這時候會因為不同的呼叫時機點而產生難以控制的行為(這裡說的父類別可能是具體類別或是抽象類別,但不包含 interface)。Effective Java 第 17 條這樣說:

Design and document for inheritance or else prohibit it.

通常這時候你應該要考慮的不是如何安全的實作 copy(),而是考慮使用組合來取代繼承。如果真的需要在繼承的類別實作 copy() 怎麼辦呢?將實作放在子類別吧,抽象類別可以選擇不實作。

Protype vs Static Factory Method

要注意這邊指的 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


上一篇
[Design Pattern] Strategy 策略模式
下一篇
[Design Pattern] Singleton 單例模式
系列文
什麼?又是/不只是 Design Patterns!?32

尚未有邦友留言

立即登入留言