Prototype Pattern(原型模式)
是什麼?
Prototype Pattern 讓你透過**複製(Clone)**一個現有的物件來建立新物件,而非透過 new 從頭建構。被複製的物件就是「原型(Prototype)」,新物件是它的副本,可以在副本上做修改而不影響原型。
ℹ️GoF 分類
Prototype 屬於建立型模式(Creational Pattern),重點在於透過複製既有物件來避免重複的初始化成本。
什麼時候用?
- 物件的建立成本很高(例如需要讀取資料庫、呼叫 API、大量計算)
- 你需要建立的新物件和現有物件只有少許差異
- 你想避免依賴具體類別的建構子(透過介面的 Clone 方法建立)
什麼時候不該用?
⚠️過度設計警告
如果物件建立成本很低(只是 new 加幾個欄位賦值),用 Prototype 反而多此一舉。另外,當物件有複雜的循環參考時,正確實作 Deep Clone 會非常困難且容易出錯。
執行流程
定義 Prototype 介面
宣告 Clone() 方法,回傳自身型別的副本
實作 Clone
在具體類別中實作深複製或淺複製邏輯
建立原型物件
建立一個完整配置好的物件作為原型
複製原型
呼叫 Clone() 取得副本
修改副本
在副本上調整需要變更的部分
流程解讀:Prototype 的核心概念是「基於既有物件建立新物件」。先建立一個完整配置好的原型物件,需要新物件時呼叫 Clone() 複製一份,再在副本上做微調。Deep Clone(深複製,遞迴複製所有巢狀物件,副本與原型完全獨立)比 Shallow Clone(淺複製,只複製第一層,巢狀物件仍共用參考)安全但成本更高。選擇取決於物件結構的複雜度。
程式碼範例
C# 版本
// 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: 3TypeScript 版本
// 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: 3Python 版本
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: 3Java 版本
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 透過 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 可能很複雜(循環參考等) |