Change Reference to Value (and back)
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”object เล็ก ๆ จำลองได้สองแบบ แบบ reference คือมี instance เดียวที่เจ้าของหลายคนถือร่วมกัน อัปเดตทีเดียวทุกคนเห็นเหมือนกัน และ identity ว่าเป็น object ตัวไหนมีความหมาย ส่วนแบบ value คือ object ที่ไม่เปลี่ยนแปลง เท่ากับตัวอื่นที่มีเนื้อหาเดียวกัน และคัดลอกได้อิสระ Change Reference to Value เปลี่ยน object ที่แชร์และแก้ค่าได้ ให้กลายเป็น value ที่ immutable และเทียบเท่ากันด้วย field ส่วนท่ากลับกันคือ Change Value to Reference ใช้เมื่อหลายจุดต้องแชร์และเฝ้าดู entity เดียวกันที่อัปเดตได้จริง ๆ
อาการของปัญหา
หัวข้อที่มีชื่อว่า “อาการของปัญหา”smell แบบ “ใช้ reference ทั้งที่ควรเป็น value” จะโผล่มาในรูป aliasing bug คือโปรแกรมสองส่วนถือ object เล็ก ๆ ตัวเดียวกัน ฝั่งหนึ่ง mutate ส่วนอีกฝั่งก็งงกับการเปลี่ยนแปลงที่ไม่เคยสั่ง อย่าง Money, Coordinate หรือ DateRange ไม่มี identity ที่มีความหมายอยู่แล้ว การแชร์จึงมีแต่จะเปิดทางให้เกิด action at a distance ส่วน smell ฝั่งตรงข้ามคือ divergent copies คือคุณมี instance แยกกันหลายตัวของสิ่งที่จริง ๆ แล้วเป็น entity เดียว เช่น record ลูกค้าคนเดียว แล้วอัปเดต copy ตัวหนึ่งไปไม่ถึงตัวอื่น
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”object Money ถูกแชร์แบบ reference การ mutate จึงรั่วไปถึง alias ทุกตัว หลัง refactor จะกลายเป็น value แบบ immutable คือ operation คืน instance ใหม่ และเทียบความเท่ากันด้วยเนื้อหา
// Before — mutable, shared by referenceclass Money { constructor(public amount: number, public currency: string) {} add(other: Money): void { this.amount += other.amount; // mutates in place, surprising aliases }}
// After — immutable value: operations return new instancesclass Money { constructor( readonly amount: number, readonly currency: string, ) {} add(other: Money): Money { return new Money(this.amount + other.amount, this.currency); } equals(other: Money): boolean { return this.amount === other.amount && this.currency === other.currency; }}# Before — mutable, shared by referenceclass Money: def __init__(self, amount, currency): self.amount = amount self.currency = currency def add(self, other): self.amount += other.amount # mutates in place
# After — frozen value: equality and hashing come from contentsfrom dataclasses import dataclass
@dataclass(frozen=True)class Money: amount: int currency: str def add(self, other): return Money(self.amount + other.amount, self.currency)// Before — pointer methods mutate a shared instancetype Money struct { Amount int Currency string}func (m *Money) Add(other Money) { m.Amount += other.Amount // mutates through the pointer}
// After — value semantics: pass and return by value, never mutatetype Money struct { Amount int Currency string}func (m Money) Add(other Money) Money { return Money{Amount: m.Amount + other.Amount, Currency: m.Currency}}// Comparable structs compare by content with ==.// Before — shared and mutated through a referencestruct Money { amount: i64, currency: String,}impl Money { fn add(&mut self, other: &Money) { self.amount += other.amount; // mutates in place }}
// After — a Copy/Clone value compared by contents#[derive(Clone, PartialEq, Eq)]struct Money { amount: i64, currency: String,}impl Money { fn add(&self, other: &Money) -> Money { Money { amount: self.amount + other.amount, currency: self.currency.clone() } }}กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”ในการเปลี่ยน reference ให้เป็น value:
- ทำให้ field ของ object ไม่เปลี่ยนแปลง —
readonly,frozen, value receiver, ไม่มี&mut - แทนที่ method ที่ mutate ใด ๆ ด้วย method ที่คืน instance ใหม่ซึ่งถือเนื้อหาที่เปลี่ยนแล้ว
- จัดให้มีความเท่ากันแบบอิงเนื้อหา (
equals,__eq__/frozen,==บน struct ที่เทียบได้,PartialEq) เพื่อให้ object สองตัวที่มี field เดียวกันเท่ากัน - ลบการแชร์ออก: ให้เจ้าของแต่ละคนถือ copy ของตัวเอง เพราะตอนนี้ copy แยกแยะกันไม่ออก
- รัน test หลังแต่ละขั้น
ในการเปลี่ยน value ให้เป็น reference (ทางกลับ) ให้นำแหล่งเดียวเข้ามา — repository หรือ registry ที่แจก instance ตัวนั้น สำหรับ identity ที่กำหนด — เพื่อให้ caller ทุกคนดึง object เดียวกัน และการอัปเดตถูกมองเห็นโดยทุกคน
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”เลือก value สำหรับแนวคิดเล็ก ๆ ที่สมบูรณ์ในตัว เช่น เงิน วันที่ พิกัด หรือช่วง ซึ่งของสองชิ้นที่เนื้อหาเท่ากันควรใช้แทนกันได้ และไม่มีใครต้องเฝ้าดูการเปลี่ยนแปลงในที่ value แบบ immutable แชร์ข้าม thread ได้อย่างปลอดภัย ตามเหตุผลได้ง่าย และไม่มีเซอร์ไพรส์จาก aliasing ต้นทุนคือต้องจอง instance ใหม่ทุกครั้งที่ “เปลี่ยนค่า” ซึ่งน้อยจนแทบไม่ต้องนับสำหรับ object เล็ก ๆ
เลือก reference เมื่อ object จำลอง entity จริงในโลกเพียงชิ้นเดียว ที่หลายส่วนของระบบต้องแชร์และเฝ้าดูตอนอัปเดต เช่น ผู้ใช้ที่ล็อกอินอยู่ หรือบัญชีที่ยอดเงินสะท้อนอยู่ในหลายมุมมอง กรณีแบบนี้ divergent copies คือบั๊กด้านความถูกต้องเลย คำถามชี้ขาดคือ ถ้าเปลี่ยน object นี้ ทุกคนที่ถืออยู่ควรเห็นการเปลี่ยนแปลงหรือเปล่า ถ้าใช่ก็ใช้ reference ถ้าไม่ และเนื้อหาเท่ากันถือว่าเป็นสิ่งเดียวกัน ก็ใช้ value
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Change Reference to Value เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| object เป็น immutable และไม่มี shared identity | object ต้องการ update ที่ทุก reference เห็นพร้อมกัน |
| ต้องการ equality ตาม content ไม่ใช่ identity | object มี lifecycle ที่ต้องจัดการ (open/close) |
| ต้องการใช้ใน map key หรือ set | หลาย component ต้อง observe การเปลี่ยนแปลงเดียวกัน |
⚠️ ไม่ควร Change Reference to Value เมื่อ:
- หลายส่วนของระบบต้องเห็น mutation เดียวกันพร้อมกัน
- object มีขนาดใหญ่มาก — copying ทุกครั้งแพงเกินไป
- object เป็น entity ที่มี identity สำคัญ (เช่น user account, database record)