Prototype
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Prototype สร้าง object ใหม่ด้วยการคัดลอก instance ที่มีอยู่และตั้งค่าไว้ครบถ้วนแล้ว ซึ่งก็คือ prototype แทนที่จะสร้างขึ้นมาใหม่ตั้งแต่ต้น
บางครั้งการสร้าง object จาก constructor ของตัวเองก็แพงหรือยุ่งยาก อาจต้องใช้การคำนวณที่แพง การตั้งค่าจำนวนมาก หรือความรู้ที่ผู้เรียกไม่มี ถ้าคุณถือ instance ที่เกือบจะเป็นอย่างที่คุณต้องการอยู่แล้ว การคัดลอก instance นั้นแล้วปรับแต่งสำเนาก็ง่ายกว่าการสร้างใหม่ Prototype ทำให้ “ขอตัวหนึ่งที่เหมือนตัวนี้” กลายเป็น first-class operation: ตัว object รู้วิธี clone ตัวเอง
การตัดสินใจที่นิยาม Prototype คือ shallow เทียบกับ deep shallow copy ทำสำเนา object ระดับบนสุด แต่แชร์ reference ที่ซ้อนอยู่ภายใน ดังนั้นการแก้ field ที่ซ้อนอยู่ในสำเนาก็จะเปลี่ยนต้นฉบับด้วย ส่วน deep copy ทำสำเนาทั้งกราฟ ทำให้สำเนาเป็นอิสระโดยสมบูรณ์ การเลือกผิดเป็นต้นเหตุคลาสสิกของบั๊ก “สำเนา” ที่แอบแชร์ list กับต้นฉบับจะสร้างความประหลาดใจให้คุณในครั้งแรกที่ฝ่ายใดฝ่ายหนึ่งถูกแก้ Prototype บังคับให้คุณเลือกสิ่งนั้นอย่างตั้งใจ
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Prototype {
<<interface>>
+clone() Prototype
}
class Document {
+title: string
+tags: List
+clone() Document
}
class Client {
+duplicate(p) Prototype
}
Prototype <|.. Document
Client ..> Prototype : clones - Prototype — interface ที่ประกาศการดำเนินการ
clone - Concrete Prototype (
Document) — implementcloneเพื่อผลิตสำเนาของตัวเอง โดยตัดสินใจว่าจะ shallow หรือ deep - Client — ได้ object ใหม่ด้วยการ clone prototype แทนที่จะเรียก constructor
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”Document ที่มี list ของ tags เรา clone แบบ deep เพื่อให้การแก้ไข tags ของสำเนาไม่กระทบต้นฉบับ
class Document { constructor( public title: string, public tags: string[], ) {}
// Deep clone: structuredClone copies the nested array too. clone(): Document { const copy = structuredClone({ title: this.title, tags: this.tags }); return new Document(copy.title, copy.tags); }}
const original = new Document('Report', ['draft', 'q2']);const copy = original.clone();copy.title = 'Report (revised)';copy.tags.push('final');
console.log(original.title, original.tags); // Report ['draft', 'q2']console.log(copy.title, copy.tags); // Report (revised) ['draft', 'q2', 'final']import copyfrom dataclasses import dataclass, field
@dataclassclass Document: title: str tags: list[str] = field(default_factory=list)
def clone(self) -> "Document": # deepcopy duplicates the nested list, so copies are independent. return copy.deepcopy(self)
original = Document("Report", ["draft", "q2"])duplicate = original.clone()duplicate.title = "Report (revised)"duplicate.tags.append("final")
print(original.title, original.tags) # Report ['draft', 'q2']print(duplicate.title, duplicate.tags) # Report (revised) ['draft', 'q2', 'final']package main
import "fmt"
type Document struct { Title string Tags []string}
// Clone returns a deep copy: the tags slice is duplicated, not shared.func (d Document) Clone() Document { tags := make([]string, len(d.Tags)) copy(tags, d.Tags) return Document{Title: d.Title, Tags: tags}}
func main() { original := Document{Title: "Report", Tags: []string{"draft", "q2"}} duplicate := original.Clone() duplicate.Title = "Report (revised)" duplicate.Tags = append(duplicate.Tags, "final")
fmt.Println(original.Title, original.Tags) // Report [draft q2] fmt.Println(duplicate.Title, duplicate.Tags) // Report (revised) [draft q2 final]}// Deriving Clone gives a deep copy: the Vec<String> is duplicated.#[derive(Clone)]struct Document { title: String, tags: Vec<String>,}
fn main() { let original = Document { title: "Report".to_string(), tags: vec!["draft".to_string(), "q2".to_string()], };
let mut duplicate = original.clone(); duplicate.title = "Report (revised)".to_string(); duplicate.tags.push("final".to_string());
println!("{} {:?}", original.title, original.tags); // Report ["draft", "q2"] println!("{} {:?}", duplicate.title, duplicate.tags); // Report (revised) ["draft", "q2", "final"]}ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”- ข้อดี: ถูกกว่าการสร้างใหม่เมื่อ object สร้างได้แพงหรือซับซ้อน
- ข้อดี: client คัดลอก object ที่มีอยู่โดยไม่ต้องพึ่งพา concrete class หรืออาร์กิวเมนต์ของ constructor ของตัวเอง
- ข้อดี: ให้คุณเก็บคลัง prototype ที่ตั้งค่าไว้ล่วงหน้าและปั๊มสร้างรูปแบบต่าง ๆ ออกมา
- ข้อเสีย: การ clone กราฟ object ที่มี cycle หรือทรัพยากรที่แชร์กัน (ไฟล์ที่เปิดอยู่, socket) เป็นเรื่องยุ่งยาก
- ข้อเสีย: ความแตกต่างระหว่าง shallow กับ deep ผิดพลาดได้ง่าย ทำให้เกิดสำเนาที่แอบแชร์ state กัน
pattern ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “pattern ที่เกี่ยวข้อง”- Builder สร้าง object ทีละขั้นตอน ส่วน Prototype เริ่มจากตัวที่ทำสำเร็จไว้แล้ว
- Abstract Factory สามารถเก็บและคืน prototype แทนที่จะสร้าง product แต่ละตัวขึ้นใหม่
| Prototype | Builder | Factory Method | |
|---|---|---|---|
| วิธีสร้าง | copy จากต้นแบบที่มีอยู่ | สร้างใหม่ทีละขั้น | สร้างจาก subclass |
| เหมาะกับ | object ที่ตั้งค่า costly หรือซับซ้อน | object ที่มี optional config เยอะ | defer การเลือก type ไปให้ subclass |
| ต้องระวัง | deep copy vs shallow copy | ลำดับ step ต้องถูกต้อง | subclass proliferation |
| ตัวอย่างใน framework | Object.assign(), spread, clone() | URLSearchParams, query builder | Connection.create(), Logger.get() |
💡 หมายเหตุสำหรับ developer
Pattern นี้พบได้บ่อยใน:
- JavaScript
structuredClone()/ object spread — clone object ที่มีอยู่แล้วแทนสร้างใหม่ทั้งหมด- Redux state updates — reducer สร้าง state ใหม่ด้วยการ clone state เดิมแล้วแก้บางส่วน (immutability ผ่าน prototype-style copy)
- Java
Cloneableinterface — บังคับให้ class ประกาศชัดว่ารองรับการ clone ตัวเอง