Prototype
สร้าง object ใหม่ด้วยการโคลน instance ที่มีอยู่
Prototype เป็น pattern การสร้าง (creational pattern) ที่ให้เรา โคลน (clone) object ที่มีอยู่แล้วเพื่อผลิต instance ใหม่ แทนการเรียก constructor ผ่าน new ตรง ๆ
ปัญหาที่ pattern นี้แก้มีอยู่สองสามแบบ:
- สร้าง object ใหม่มีต้นทุนสูง — เช่นต้อง query database, เรียก network, หรือคำนวณหนัก ๆ ก่อนได้ object ที่ใช้งานได้ ถ้ามี object พร้อมใช้อยู่แล้วสักตัว การโคลนมันย่อมถูกกว่าสร้างใหม่จากศูนย์
- client รู้จักแค่ interface ไม่รู้จัก concrete class — เมื่อ object มาจาก third-party หรือถูกโหลดแบบ dynamic เราอาจไม่มีสิทธิ์เรียก constructor ของมันตรง ๆ ได้เลย รู้แต่ว่ามันมี method
Clone() - field บางส่วนเป็น private — การ copy object จากภายนอกด้วยการอ่าน field ทีละตัวทำไม่ได้ เพราะเข้าไม่ถึง มีแต่ตัว object เองเท่านั้นที่รู้วิธี copy ตัวเองอย่างถูกต้อง
- ต้องการ instance ที่ config ไว้เกือบเหมือนกันจำนวนมาก — แทนที่จะมี subclass เต็มไปหมดสำหรับแต่ละ config ให้เตรียม object ต้นแบบ (prototype) ไว้หนึ่งตัวต่อ config แล้วโคลนตามต้องการ
แนวคิดหลักคือย้ายความรับผิดชอบเรื่อง “จะสร้างสำเนาของตัวเองอย่างไร” ไปไว้ที่ตัว object เอง ผ่าน method clone ที่มันประกาศไว้ใน interface กลาง — คล้ายกับ Factory Method ตรงที่ทั้งคู่คืน object ใหม่โดยไม่ผูก client เข้ากับ concrete class แต่ Prototype ใช้ instance ที่มีอยู่เป็นแม่แบบ ในขณะที่ Factory Method สร้างจาก logic ล้วน ๆ
การโคลนมีสองระดับที่ต้องแยกให้ชัด:
- shallow copy — คัดลอกเฉพาะ field ระดับบนสุด ส่วน field ที่เป็น reference type (เช่น list, object ซ้อน) จะยังชี้ไปที่ object เดิม ทำให้แก้ไขต้นฉบับหรือสำเนาตัวใดตัวหนึ่งกระทบอีกตัวโดยไม่ตั้งใจ
- deep copy — คัดลอกทั้ง object graph รวมถึง object ที่ซ้อนอยู่ข้างในด้วย ทำให้ต้นฉบับกับสำเนาเป็นอิสระต่อกันอย่างสมบูรณ์ แต่มีต้นทุนสูงกว่าและต้องระวัง circular reference
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Client {
+Operation()
}
class IPrototype {
+Clone() IPrototype
}
class ConcretePrototypeA {
-fieldA
+Clone() IPrototype
}
class ConcretePrototypeB {
-fieldB
+Clone() IPrototype
}
Client --> IPrototype
IPrototype <|.. ConcretePrototypeA
IPrototype <|.. ConcretePrototypeB
- IPrototype — interface กลางที่ประกาศ method
Clone()(ใน .NET มักใช้ICloneableหรือ interface กำหนดเองที่คืนชนิดที่ strong-typed กว่า) - ConcretePrototypeA / B — class จริงที่รู้วิธี copy field ของตัวเอง รวมถึงตัดสินใจว่า field ไหนต้อง shallow หรือ deep copy
- Client — ถือ reference ไปที่
IPrototypeเท่านั้น ไม่จำเป็นต้องรู้ concrete type เพื่อสร้างสำเนาใหม่
รูปแบบขยายที่พบบ่อยคือ Prototype Registry — เก็บ prototype ที่ใช้บ่อยไว้ใน dictionary ชื่อ → instance แล้วให้ client ขอ clone ผ่านชื่อ แทนการถือ reference ตรง ๆ
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”ลำดับการทำงานเมื่อ client ต้องการ object ใหม่ที่หน้าตาคล้ายของเดิม:
sequenceDiagram
participant Client
participant Original as ConcretePrototype ต้นฉบับ
participant Copy as ConcretePrototype สำเนา
Client->>Original: Clone()
Original->>Original: MemberwiseClone หรือ copy constructor
Original->>Copy: สร้างสำเนาของ field แต่ละตัว
Original-->>Client: คืน Copy
Client->>Copy: ปรับแต่งค่าเฉพาะที่ต้องการ
- Client เรียก
Clone()บน instance ที่มีอยู่ (prototype) โดยไม่ต้องรู้ concrete class - ตัว prototype รับผิดชอบการ copy ตัวเอง — ใน .NET มักเริ่มจาก
Object.MemberwiseClone()ซึ่งทำ shallow copy ให้ฟรี แล้วค่อย deep-copy เฉพาะ field ที่เป็น reference type ที่ไม่อยาก share กัน - ได้ instance ใหม่ที่ state เริ่มต้นเหมือนต้นฉบับทุกประการ
- Client ปรับแต่งค่าบางส่วนบนสำเนาต่อได้ตามต้องการ โดยไม่กระทบต้นฉบับ (ถ้า deep copy ถูกต้อง)
จุดที่ผิดพลาดบ่อยที่สุดคือลืมว่า field ไหนเป็น reference type แล้วปล่อยให้เป็น shallow copy โดยไม่ตั้งใจ — สำเนาสองตัวจะ share list หรือ object ซ้อนตัวเดียวกัน แก้ไขฝั่งหนึ่งกระทบอีกฝั่งทันที
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่างนี้แสดง Document ที่มี field เป็น value type, string (immutable ใน .NET จึง share reference ได้อย่างปลอดภัย), และ List<string> กับ object ซ้อน Author ที่ต้อง deep copy อย่างระมัดระวัง เพื่อไม่ให้สำเนากับต้นฉบับ share ข้อมูลเดียวกัน
// Author เป็น object ซ้อนที่ต้อง deep copy ด้วย ไม่งั้นสำเนาจะยังชี้ Author ตัวเดิมpublic class Author{ public string Name { get; set; } public string Email { get; set; }
public Author Clone() => new Author { Name = Name, Email = Email };}
// interface กำหนดเองที่ strong-typed กว่า ICloneable ของ .NETpublic interface IPrototype<T>{ T Clone();}
public class Document : IPrototype<Document>{ public string Title { get; set; } public Author Author { get; set; } public List<string> Tags { get; set; } = new();
// deep copy: คัดลอก Author และ Tags แยกเป็นอิสระจากต้นฉบับ public Document Clone() => new Document { Title = Title, // string เป็น immutable ใน .NET share reference ได้อย่างปลอดภัย Author = Author?.Clone(), // ต้อง deep copy object ซ้อน ไม่งั้นแก้ผู้เขียนของสำเนาจะกระทบต้นฉบับ Tags = new List<string>(Tags) // ต้องสร้าง list ใหม่ ไม่ใช่ share list เดียวกัน };}
// ตัวอย่างการใช้งาน: เตรียม template แล้วโคลนไปปรับแต่งต่อvar template = new Document{ Title = "รายงานประจำเดือน", Author = new Author { Name = "กานต์", Email = "kan@example.com" }, Tags = new List<string> { "draft", "internal" }};
var report = template.Clone();report.Title = "รายงานประจำเดือนกรกฎาคม";report.Tags.Add("2026-07");
// template.Tags ยังมีแค่ ["draft", "internal"] เพราะ Clone() ทำ deep copy ของ Tags ไว้แล้วConsole.WriteLine(template.Tags.Count); // 2Console.WriteLine(report.Tags.Count); // 3ตัวอย่าง Prototype Registry อย่างง่าย สำหรับกรณีมี prototype สำเร็จรูปหลายแบบให้เลือกโคลนตามชื่อ:
public class DocumentRegistry{ private readonly Dictionary<string, Document> _prototypes = new();
public void Register(string key, Document prototype) => _prototypes[key] = prototype;
public Document Create(string key) => _prototypes[key].Clone();}
// การใช้งานvar registry = new DocumentRegistry();registry.Register("internal-memo", template);
var memo = registry.Create("internal-memo"); // ได้สำเนาใหม่ทุกครั้ง ไม่ใช่ instance เดียวกันเมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- object ที่จะสร้างมี state เริ่มต้นซับซ้อน หรือต้องผ่านการคำนวณ/เรียก I/O ที่มีต้นทุนสูงก่อนพร้อมใช้งาน
- ต้องการ instance หลายตัวที่ config เกือบเหมือนกันทุกประการ ต่างกันแค่รายละเอียดปลีกย่อย
- client รู้จัก object ผ่าน interface เท่านั้น ไม่รู้ concrete class (เช่น object มาจาก plugin หรือถูกโหลดแบบ dynamic)
- ต้องการหลีกเลี่ยง class hierarchy ที่บวมขึ้นเพราะสร้าง subclass แยกสำหรับทุก config ที่เป็นไปได้
- ระบบต้องการสร้าง object ใหม่ตอน runtime โดยยังไม่รู้ type ที่แน่นอนล่วงหน้าตอน compile time
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- object graph มี circular reference — การ deep copy จะซับซ้อนและเสี่ยง stack overflow หรือ infinite loop ถ้าไม่จัดการ reference ที่เคย copy ไปแล้ว
- object เรียบง่าย สร้างด้วย constructor ตรง ๆ ก็ชัดเจนและถูกกว่าอยู่แล้ว — การเพิ่ม
Clone()เข้ามาเป็นความซับซ้อนที่ไม่จำเป็น - ทีมไม่สามารถรักษาวินัยเรื่อง shallow กับ deep copy ให้สม่ำเสมอได้ — ความกำกวมของความหมาย
Clone()เป็นแหล่ง bug ที่พบบ่อย โดยเฉพาะเมื่อใช้ICloneableของ .NET ที่ไม่บังคับว่าต้อง deep หรือ shallow - ในบริบทที่ immutable object เหมาะกว่าอยู่แล้ว — ถ้า object เป็น Value Object ที่ immutable การ “โคลนแล้วแก้” อาจไม่มีประโยชน์เท่าการสร้าง instance ใหม่ตรง ๆ ด้วยค่าที่ต่างออกไป
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | ไม่ผูก client เข้ากับ concrete class ของ object ที่จะสร้าง |
| ข้อดี | ลด code initialization ที่ซ้ำซ้อน โดยโคลนจาก instance ที่ config ไว้แล้ว |
| ข้อดี | เพิ่ม/ลบ prototype ได้ตอน runtime โดยไม่ต้องแก้ code client |
| ข้อดี | เป็นทางเลือกแทนการสร้าง subclass จำนวนมากสำหรับแต่ละ config |
| ข้อเสีย | object ที่มี circular reference โคลนยาก ต้องจัดการ reference ที่เคย copy แล้วเพื่อไม่ให้วนซ้ำ |
| ข้อเสีย | ทุก concrete class ต้อง implement Clone() เอง — เพิ่มภาระบำรุงรักษา |
| ข้อเสีย | ความหมายของ Clone() กำกวมได้ถ้าไม่มีข้อตกลงชัดเจนว่า shallow หรือ deep — Microsoft เองก็แนะนำว่าไม่ควรใช้ ICloneable ใน public API เพราะพฤติกรรมไม่แน่นอน |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Factory Method — อีก pattern การสร้างที่ไม่ผูก client เข้ากับ concrete class แต่สร้างจาก logic แทนการโคลน
- Abstract Factory — จัดกลุ่ม factory method หลายตัวไว้ด้วยกันสำหรับตระกูล object ที่เกี่ยวข้องกัน
- Builder — สร้าง object ซับซ้อนทีละขั้นตอน เหมาะกับกรณีที่ prototype สำเร็จรูปยังไม่มีให้โคลน
- Object Mother — เทคนิคสร้าง test object สำเร็จรูป มักใช้ร่วมกับแนวคิด prototype ในการเตรียม test fixture
- Memento — เก็บ snapshot ของ state object ไว้กู้คืนภายหลัง ต่างจาก Prototype ตรงที่เป้าหมายคือ undo/restore ไม่ใช่สร้าง instance ใหม่
- Value Object — object ที่ immutable ซึ่งมักไม่จำเป็นต้อง “โคลน” เพราะสร้าง instance ใหม่ด้วยค่าที่ต่างกันได้โดยตรง