ข้าม​ไป​ยัง​เนื้อหา

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: ปรับแต่งค่าเฉพาะที่ต้องการ
  1. Client เรียก Clone() บน instance ที่​มี​อยู่ (prototype) โดย​ไม่​ต้อง​รู้ concrete class
  2. ตัว prototype รับผิดชอบ​การ copy ตัวเอง — ใน .NET มัก​เริ่ม​จาก Object.MemberwiseClone() ซึ่ง​ทำ shallow copy ให้​ฟรี แล้ว​ค่อย deep-copy เฉพาะ field ที่​เป็น reference type ที่​ไม่​อยาก share กัน
  3. ได้ instance ใหม่​ที่ state เริ่มต้น​เหมือน​ต้นฉบับ​ทุก​ประการ
  4. Client ปรับ​แต่ง​ค่า​บาง​ส่วน​บน​สำเนา​ต่อ​ได้​ตาม​ต้องการ โดย​ไม่​กระทบ​ต้นฉบับ (ถ้า deep copy ถูกต้อง)

จุด​ที่​ผิดพลาด​บ่อย​ที่สุด​คือ​ลืม​ว่า field ไหน​เป็น reference type แล้ว​ปล่อย​ให้​เป็น shallow copy โดย​ไม่​ตั้งใจ — สำเนา​สอง​ตัว​จะ share list หรือ object ซ้อน​ตัว​เดียวกัน แก้ไข​ฝั่ง​หนึ่ง​กระทบ​อีก​ฝั่ง​ทันที

ตัวอย่าง​นี้​แสดง Document ที่​มี field เป็น value type, string (immutable ใน .NET จึง share reference ได้​อย่าง​ปลอดภัย), และ List&lt;string&gt; กับ 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 ของ .NET
public 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); // 2
Console.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 ใหม่​ด้วย​ค่าที่​ต่าง​กัน​ได้​โดยตรง