Prototype Pattern(原型模式)

是什麼?

Prototype Pattern 讓你透過**複製(Clone)**一個現有的物件來建立新物件,而非透過 new 從頭建構。被複製的物件就是「原型(Prototype)」,新物件是它的副本,可以在副本上做修改而不影響原型。

ℹ️GoF 分類

Prototype 屬於建立型模式(Creational Pattern),重點在於透過複製既有物件來避免重複的初始化成本。

什麼時候用?

什麼時候不該用?

⚠️過度設計警告

如果物件建立成本很低(只是 new 加幾個欄位賦值),用 Prototype 反而多此一舉。另外,當物件有複雜的循環參考時,正確實作 Deep Clone 會非常困難且容易出錯。

執行流程

1

定義 Prototype 介面

宣告 Clone() 方法,回傳自身型別的副本

2

實作 Clone

在具體類別中實作深複製或淺複製邏輯

3

建立原型物件

建立一個完整配置好的物件作為原型

4

複製原型

呼叫 Clone() 取得副本

5

修改副本

在副本上調整需要變更的部分

流程解讀:Prototype 的核心概念是「基於既有物件建立新物件」。先建立一個完整配置好的原型物件,需要新物件時呼叫 Clone() 複製一份,再在副本上做微調。Deep Clone(深複製,遞迴複製所有巢狀物件,副本與原型完全獨立)比 Shallow Clone(淺複製,只複製第一層,巢狀物件仍共用參考)安全但成本更高。選擇取決於物件結構的複雜度。

程式碼範例

C# 版本

csharp
// 1. Prototype 介面
public interface IDocumentPrototype
{
    IDocumentPrototype Clone();
}
 
// 2. 具體原型
public class ReportTemplate : IDocumentPrototype
{
    public string Title { get; set; } = "";
    public string Header { get; set; } = "";
    public string Footer { get; set; } = "";
    public List<string> Sections { get; set; } = new();
    public Dictionary<string, string> Styles { get; set; } = new();
 
    // Deep Clone
    public IDocumentPrototype Clone()
    {
        return new ReportTemplate
        {
            Title = this.Title,
            Header = this.Header,
            Footer = this.Footer,
            Sections = new List<string>(this.Sections),
            Styles = new Dictionary<string, string>(this.Styles)
        };
    }
 
    public override string ToString() =>
        $"Report: {Title} | Sections: {Sections.Count} | Styles: {Styles.Count}";
}
 
// 3. 使用
var template = new ReportTemplate
{
    Title = "月報範本",
    Header = "公司機密",
    Footer = "第 {page} 頁",
    Sections = new List<string> { "摘要", "本月數據", "下月計畫" },
    Styles = new Dictionary<string, string> { ["font"] = "Arial", ["size"] = "12pt" }
};
 
// 複製後修改
var januaryReport = (ReportTemplate)template.Clone();
januaryReport.Title = "2024 年 1 月報";
januaryReport.Sections.Add("特別事項:農曆新年");
 
var februaryReport = (ReportTemplate)template.Clone();
februaryReport.Title = "2024 年 2 月報";
 
Console.WriteLine(template);        // Report: 月報範本 | Sections: 3
Console.WriteLine(januaryReport);   // Report: 2024 年 1 月報 | Sections: 4
Console.WriteLine(februaryReport);  // Report: 2024 年 2 月報 | Sections: 3

TypeScript 版本

typescript
// 1. Prototype 介面
interface Cloneable<T> {
  clone(): T;
}
 
// 2. 具體原型
class ReportTemplate implements Cloneable<ReportTemplate> {
  constructor(
    public title: string = "",
    public header: string = "",
    public footer: string = "",
    public sections: string[] = [],
    public styles: Record<string, string> = {}
  ) {}
 
  clone(): ReportTemplate {
    return new ReportTemplate(
      this.title,
      this.header,
      this.footer,
      [...this.sections],
      { ...this.styles }
    );
  }
 
  toString(): string {
    return `Report: ${this.title} | Sections: ${this.sections.length}`;
  }
}
 
// 3. 使用
const template = new ReportTemplate(
  "月報範本", "公司機密", "第 {page} 頁",
  ["摘要", "本月數據", "下月計畫"],
  { font: "Arial", size: "12pt" }
);
 
const januaryReport = template.clone();
januaryReport.title = "2024 年 1 月報";
januaryReport.sections.push("特別事項:農曆新年");
 
const februaryReport = template.clone();
februaryReport.title = "2024 年 2 月報";
 
console.log(template.toString());        // Report: 月報範本 | Sections: 3
console.log(januaryReport.toString());   // Report: 2024 年 1 月報 | Sections: 4
console.log(februaryReport.toString());  // Report: 2024 年 2 月報 | Sections: 3

Python 版本

python
import copy
from dataclasses import dataclass, field
 
# 1. 具體原型(Python 用 copy 模組)
@dataclass
class ReportTemplate:
    title: str = ""
    header: str = ""
    footer: str = ""
    sections: list = field(default_factory=list)
    styles: dict = field(default_factory=dict)
 
    def clone(self) -> "ReportTemplate":
        return copy.deepcopy(self)
 
    def __str__(self) -> str:
        return f"Report: {self.title} | Sections: {len(self.sections)}"
 
# 2. 使用
template = ReportTemplate(
    title="月報範本",
    header="公司機密",
    footer="第 {page} 頁",
    sections=["摘要", "本月數據", "下月計畫"],
    styles={"font": "Arial", "size": "12pt"},
)
 
january_report = template.clone()
january_report.title = "2024 年 1 月報"
january_report.sections.append("特別事項:農曆新年")
 
february_report = template.clone()
february_report.title = "2024 年 2 月報"
 
print(template)         # Report: 月報範本 | Sections: 3
print(january_report)   # Report: 2024 年 1 月報 | Sections: 4
print(february_report)  # Report: 2024 年 2 月報 | Sections: 3

Java 版本

java
import java.util.*;
 
// 1. Prototype 介面
public interface DocumentPrototype {
    DocumentPrototype clone();
}
 
// 2. 具體原型
public class ReportTemplate implements DocumentPrototype {
    private String title;
    private String header;
    private String footer;
    private List<String> sections;
    private Map<String, String> styles;
 
    public ReportTemplate(String title, String header, String footer,
                          List<String> sections, Map<String, String> styles) {
        this.title = title;
        this.header = header;
        this.footer = footer;
        this.sections = sections;
        this.styles = styles;
    }
 
    @Override
    public ReportTemplate clone() {
        return new ReportTemplate(
            this.title, this.header, this.footer,
            new ArrayList<>(this.sections),
            new HashMap<>(this.styles)
        );
    }
 
    public void setTitle(String title) { this.title = title; }
    public List<String> getSections() { return sections; }
 
    @Override
    public String toString() {
        return "Report: " + title + " | Sections: " + sections.size();
    }
}
 
// 3. 使用
ReportTemplate template = new ReportTemplate(
    "月報範本", "公司機密", "第 {page} 頁",
    new ArrayList<>(List.of("摘要", "本月數據", "下月計畫")),
    new HashMap<>(Map.of("font", "Arial", "size", "12pt"))
);
 
ReportTemplate januaryReport = template.clone();
januaryReport.setTitle("2024 年 1 月報");
januaryReport.getSections().add("特別事項:農曆新年");
 
ReportTemplate februaryReport = template.clone();
februaryReport.setTitle("2024 年 2 月報");
 
System.out.println(template);        // Report: 月報範本 | Sections: 3
System.out.println(januaryReport);   // Report: 2024 年 1 月報 | Sections: 4
System.out.println(februaryReport);  // Report: 2024 年 2 月報 | Sections: 3

結構圖

Client
clone()
Prototype (interface)
implements
ReportTemplate (ConcretePrototype)
creates copy
Cloned Object

結構解讀:Client 透過 Prototype 介面的 Clone() 方法建立新物件,不需要知道具體類別的建構子。ConcretePrototype 實作 Clone() 來複製自身的所有狀態。複製出的 Cloned Object 是獨立的副本,修改副本不影響原型。這種方式讓 Client 完全解耦於具體類別的建構細節。

實戰補充

💡資深開發者經驗

Deep Clone vs Shallow Clone:Shallow Clone 只複製第一層,參考型別(List、Dictionary)仍指向同一份資料。Deep Clone 會遞迴複製所有層級。大多數情境你需要的是 Deep Clone,否則修改副本會影響原型。

C# ICloneable 的陷阱:C# 內建的 ICloneable 介面回傳 object,沒有泛型支援,也沒有規定是 Deep 還是 Shallow Clone。建議自定義 IPrototype<T> 介面,明確語意。

Prototype Registry:可以用一個 Dictionary 存放常用的原型,需要時用 key 查詢並 Clone。這在遊戲開發中很常見 — 預設一組怪物原型,生成新怪物時直接複製。

序列化做 Deep Clone:懶人版 Deep Clone 可以用 JSON 序列化再反序列化:JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(obj))。效能較差但實作簡單,適合不頻繁的場景。

理解測驗

🤔 Prototype Pattern 的核心機制是什麼?

🤔 Deep Clone 和 Shallow Clone 的差別是什麼?

🤔 以下哪個場景最適合用 Prototype Pattern?

面試常見問題

Q: 實作 Deep Clone 有哪些方式?各有什麼優缺點?

A: (1) 手動逐層複製 — 最精確但最繁瑣,每新增欄位都要記得更新 Clone 方法。(2) 序列化/反序列化 — JsonSerializer.Deserialize(JsonSerializer.Serialize(obj)) 簡單但效能差。(3) 反射(Reflection)— 通用但效能差且遇到特殊型別可能出錯。(4) Source Generator — .NET 可用 Source Generator 自動產生 Clone 方法,兼顧效能和維護性。

Q: Prototype 和 Factory Method 都是建立型模式,怎麼選?

A: Factory Method 透過子類別覆寫來決定建立什麼,適合「根據條件選擇不同類別」。Prototype 透過複製來建立,適合「新物件和現有物件只有少許差異」。選擇標準:如果新物件需要和某個既有物件幾乎一樣 → Prototype;如果需要根據條件建立完全不同的物件 → Factory Method。

相關模式

| 模式 | 關係 | |------|------| | Factory Method / Abstract Factory | 都是建立型模式。Factory 透過繼承決定「建什麼」,Prototype 透過複製建立「跟誰像」 | | Memento | 都涉及物件狀態的拍照,但 Prototype Clone 的目的是建立新物件,Memento 的目的是回復舊狀態 | | Composite | Composite 結構中的節點可以用 Prototype Clone 整棵子樹 | | Decorator | 需要 Clone 帶有 Decorator 的物件時,必須確保整條裝飾鏈都被正確複製 |

重點整理

💡一句話記住

Prototype Pattern = 影印機:複製一份改個名字,比從白紙重畫快。 口訣:「複製比建立快,記得用 Deep Clone」

| 概念 | 說明 | |------|------| | Prototype(原型介面) | 定義 Clone() 方法 | | ConcretePrototype(具體原型) | 實作 Clone(),複製自身 | | Deep Clone | 遞迴複製所有層級,副本完全獨立 | | Shallow Clone | 只複製第一層,巢狀物件仍共用 | | 核心好處 | 避免昂貴的初始化成本 + 不依賴具體類別的建構子 | | 代價 | 正確實作 Deep Clone 可能很複雜(循環參考等) |

你可能也想看

Builder PatternAdapter Pattern

按 ← → 鍵切換課程