今天來聊聊 Prototype 原型模式
我們來看看原文的定義,來自 Design Patterns: Elements of Reusable Object-Oriented Software
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
用中文理解可以是
先準備好一個「原型(Prototype)」實體當範本,之後要建立新物件時,不透過建構子重新跑一次建構流程,而是直接複製這個範本,再依需求做局部調整
核心特性
先舉個生活化的例子,找工作投履歷的時候,我們通常會先準備好一份「範本履歷」,姓名、學歷、大部分的技能都先填好
public class Resume
{
public string Name { get; set; }
public string Title { get; set; }
public int YearsOfExperience { get; set; }
// 自定義 Clone(),回傳型別 Resume
public Resume Clone()
{
return (Resume)this.MemberwiseClone(); // MemberwiseClone() C# 原生方法,回傳的是 object,這裡轉型一次就好
}
}
每次要投一間新公司,就複製這份範本,只改職稱這個欄位就好,其他不用重打一次
var baseResume = new Resume { Name = "阿柴", Title = "全端工程師", YearsOfExperience = 1 };
var resumeForA = baseResume.Clone();
resumeForA.Title = "後端工程師"; // 投 A 公司時想掛的職稱
var resumeForB = baseResume.Clone();
resumeForB.Title = "前端工程師"; // 投 B 公司時想掛的職稱
Console.WriteLine(baseResume.Title); // 全端工程師(範本沒被動到)
Console.WriteLine(resumeForA.Title); // 後端工程師
Console.WriteLine(resumeForB.Title); // 前端工程師
每份複製出來的履歷都是獨立的,改一份不會動到另一份
等等等,你是不是也會想到,改這麼麻煩,我不直接 new 三個實體就好了?
說的也是....(欸不是),因為這是簡單的範例(我比較好懂),但實際上來說,軟體結構是非常複雜且龐大的
並不是通通 new 就可以治百病,我們可以來初步判斷什麼時候不能/不想直接 new?
建立成本太高(昂貴的初始化)
如果 new Resume() 的建構子內部需要讀設定檔、打 API 查資料庫、或是跑演算法計算(耗時 500ms),直接 new 3 次就要耗費 1.5 秒
用 Clone() 是直接在記憶體層級拷貝現有狀態,耗時幾乎是 0ms
初始化狀態極其繁瑣(20+ 個欄位預設值)
假設履歷有 30 個欄位,其中 28 個欄位(學歷、自傳、聯絡電話、專案經驗、排版設定...)在不同版本間都一模一樣,只有 2 個欄位要改
若直接 new,每開一個新實體就得手動重新 assign 28 次相同屬性,程式碼會充滿冗長且重複的賦值邏輯
用 Clone() 則是一行取得包含 28 個預設值的新物件,再改那 2 個即可
呼叫端不知道(也不該知道)具體類別與建構細節
有時你的程式碼只拿到一個介面(例如 IResume template),你不知道它背後是哪一個具體的 Class,它可能是 FrontendResume、BackendResume 甚至 SpecialFormatResume,這時你根本無法寫 new ???(),但只要有 Prototype 介面,直接呼叫 template.Clone() 就能完美複製出正確型別的實體
好,那我們一樣回到柴咖啡
某天下午兩點,隔壁科技公司的福委傳來一張下午茶團購訂單:「招牌柴拿鐵 20 杯!」
小黑樂了,因為這個科技公司點好多次,但店員苦哈哈,魔鬼藏在細節裡
小明:少冰、無糖、加肉桂粉
小華:去冰、半糖、換燕麥奶
公司老闆:熱、正常糖、加一份濃縮、加肉桂粉
...底下還有 17 個人各有各的微調,好不快樂
這個「招牌柴拿鐵」可講究了,系統裡設定了將近十個繁瑣欄位:選豆產區、烘焙度、義式萃取秒數、預設水溫、奶泡綿密度、預設拉花圖案...
阿柴想看看以前都怎麼處理訂單的
// 小黃留下來的歷史遺產:每杯都從零 new,然後一行一行複製貼上設定
var coffee1 = new ChaiLatte();
coffee1.BeanOrigin = "衣索比亞 耶加雪菲";
coffee1.RoastLevel = "中淺焙";
coffee1.BrewTemp = 92;
coffee1.MilkRatio = 0.7;
coffee1.FoamDensity = "極綿密";
coffee1.Ice = "少冰";
coffee1.Sugar = "無糖";
coffee1.Toppings.Add("肉桂粉");
var coffee2 = new ChaiLatte();
coffee2.BeanOrigin = "衣索比亞 耶加雪菲";
coffee2.RoastLevel = "中淺焙";
coffee2.BrewTemp = 92;
coffee2.MilkRatio = 0.7;
coffee2.FoamDensity = "極綿密";
coffee2.Ice = "去冰";
coffee2.Sugar = "半糖";
coffee2.MilkType = "燕麥奶";
// ...底下還有 18 杯要複製貼上
阿柴這時候心想,所以我要手動複製20筆訂單嗎,如果哪天要改咖啡豆產區,是不是又要改20次?
阿柴決定不再當複製貼上戰士,是時候用 Prototype 原型模式來拯救這堆遺產程式碼了!
既然 20 杯都是以「標準招牌柴拿鐵」為基底,何不先準備一個「拿鐵原型(Prototype)」?
每次來新訂單,直接拷貝一份出來,只改客人指定的客製化項目就好!
public class ChaiLatte
{
// 繁瑣的固定基底參數
public string BeanOrigin { get; set; } = "衣索比亞 耶加雪菲";
public string RoastLevel { get; set; } = "中淺焙";
public int BrewTemp { get; set; } = 92;
public double MilkRatio { get; set; } = 0.7;
public string FoamDensity { get; set; } = "極綿密";
// 容易變動的客製化選項
public string Sugar { get; set; } = "正常糖";
public string Ice { get; set; } = "正常冰";
public string MilkType { get; set; } = "全脂鮮乳";
public List<string> Toppings { get; set; } = new List<string>();
// 實作 Prototype 拷貝方法
public ChaiLatte Clone()
{
return (ChaiLatte)this.MemberwiseClone();
}
}
現在要處理那 20 杯訂單,程式碼變得乾淨好讀
// 1. 建立一杯預先設定好的原型範本
var standardLatte = new ChaiLatte();
// 2. 小明的訂單:直接複製範本,只改微調項目
var mingCoffee = standardLatte.Clone();
mingCoffee.Ice = "少冰";
mingCoffee.Sugar = "無糖";
mingCoffee.Toppings.Add("肉桂粉");
// 3. 小華的訂單:同樣複製範本
var huaCoffee = standardLatte.Clone();
huaCoffee.Ice = "去冰";
huaCoffee.Sugar = "半糖";
huaCoffee.MilkType = "燕麥奶";
這時候阿柴心滿意足:「不用每次都在那邊重複設定 5~6 個繁瑣的烘焙萃取參數了!改哪裡點哪裡,誣告讚!」
說時遲那時快,阿柴收到了小黑的抱怨:「 為什麼小華的燕麥奶拿鐵裡面,也被加了肉桂粉...」
阿柴:「???」
阿柴趕緊把每杯咖啡的配料清單印出來看:
Console.WriteLine($"小明配料: {string.Join(", ", mingCoffee.Toppings)}");
Console.WriteLine($"小華配料: {string.Join(", ", huaCoffee.Toppings)}");
Console.WriteLine($"範本配料: {string.Join(", ", standardLatte.Toppings)}");
出來的結果讓阿柴倒抽一口氣:
明明只有小明加了肉桂粉,怎麼連小華、甚至是作為範本的 standardLatte 都有肉桂粉了?
朋友們,這就是 Prototype 模式容易踩的地雷,淺拷貝(Shallow Copy) vs 深拷貝(Deep Copy)
有學過 JS 的朋友,應該對淺拷貝與深拷貝不陌生
在 C# 裡,MemberwiseClone() 做的是淺拷貝:
值型別(Value Type,如 int, double):會直接複製一份全新的數值過去,改了互不影響
字串(string):雖然是參考型別,但因為不可變(immutable)特性,修改時會建立新字串,所以也看似獨立
但參考型別(Reference Type,如 List, 陣列、自訂物件):只會複製「記憶體指標(Reference)」
這意味著:standardLatte、mingCoffee 和 huaCoffee 的 Toppings 屬性,實際都指向記憶體中同一條 List
// 因為是淺拷貝,List 複製的只是記憶體位址,並沒有在記憶體裡建立一條新的 List,所以也觸發了狀態汙染(State Pollution)
mingCoffee.Toppings.Add("肉桂粉");
小明把肉桂粉使用.Add()丟進清單,等於所有人看到的配料碗裡都會多了一把肉桂粉
要解決這個問題,我們必須確保複製時,連同內部的參考型別物件也一併建立全新的複製品:
public class ChaiLatte
{
// ...前面屬性保持不變
public List<string> Toppings { get; set; } = new List<string>();
public ChaiLatte Clone()
{
// 1. 先透過 MemberwiseClone 複製基本型別
var clone = (ChaiLatte)this.MemberwiseClone();
// 2. 手動為參考型別(List)建立一份全新實體(深拷貝)
clone.Toppings = new List<string>(this.Toppings);
return clone;
}
}
改完之後再次執行:
var standardLatte = new ChaiLatte();
var mingCoffee = standardLatte.Clone();
mingCoffee.Toppings.Add("肉桂粉");
var huaCoffee = standardLatte.Clone();
Console.WriteLine($"小明配料: {string.Join(", ", mingCoffee.Toppings)}"); // 肉桂粉
Console.WriteLine($"小華配料: {string.Join(", ", huaCoffee.Toppings)}"); // (空的,正常了)
這次確定只有小明有肉桂粉,其他小華跟原型 standardLatte 都不受影響
如同上述我們採到的坑,先判斷欄位是值型別還是參照型別,才知道淺拷貝夠不夠:Latte 的 Size、IceLevel、SugarLevel、MilkType 都是 enum(值型別),淺拷貝直接複製就沒問題
但只要有一個參照型別欄位(List、另一個 class 的實例),就得針對那個欄位手動處理,不然會共享同一份內部資料
深拷貝不用對所有欄位都下手:如果每加一個欄位就把整個物件都寫成手動深拷貝,維護成本只會往上堆
只對參照型別的欄位處理就好,值型別欄位交給 MemberwiseClone() 就夠了
原型要維持乾淨,才能被安全複製:standardLatte 是所有訂單共用的原型,一旦被某張訂單意外改到,後面每一杯複製出來的都會帶著錯誤的初始狀態
這也是為什麼「複製出來的東西彼此獨立、不會互相污染」是 Prototype 的核心特性之一
設計模式就像調配咖啡的糖漿,加對了是風味,加過頭就只剩死甜,看到「複製」很方便,但不代表專案裡所有的 new 都要換成 Clone():
物件結構單純、建立成本極低時
如果一個 class 只有 2~3 個基本欄位,建構子也不讀檔、不查 DB,直接 new SimpleCoffee() 既直觀又乾淨
硬要為了套模式加上 Clone(),只是徒增維護負擔
內部包含極端複雜的深層巢狀物件(Object Graph)
如果一個物件內部包了 5 層參考型別、甚至有循環引用(A 參照 B,B 又參照 A),手動實作深拷貝會是一場災難。每次新增或修改子類別欄位,Clone() 漏改一個地方就會引發隱性 Bug
跟其他建立型模式(Creational Patterns)的邊界:
Factory(工廠模式):適合「從零依不同條件,決定產出哪種具體子類別」
Builder(建造者模式):適合「物件建構過程極其複雜,需要一步步組裝零件」
Prototype(原型模式):適合「已經有一個組裝好的範本,後續產出只是要拿現成狀態做『微調與衍伸』」。
判斷標準:
創建型五天王講得差不多了,那明天我們來攤開來做個總結對比,看看它們各自解決什麼問題、又是怎麼分工的
如果看不懂我寫的,可以看看延伸的資料,希望可以幫助到你
Design Pattern: Creational Patterns — Prototype Pattern (原型模式)
原型模式 Prototype Pattern|先複製一份,再微調就好