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

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 reference
class 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 instances
class 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;
}
}

ในการเปลี่ยน reference ให้เป็น value:

  1. ทำให้ field ของ object ไม่เปลี่ยนแปลง — readonly, frozen, value receiver, ไม่มี &mut
  2. แทนที่ method ที่ mutate ใด ๆ ด้วย method ที่คืน instance ใหม่ซึ่งถือเนื้อหาที่เปลี่ยนแล้ว
  3. จัดให้มีความเท่ากันแบบอิงเนื้อหา (equals, __eq__/frozen, == บน struct ที่เทียบได้, PartialEq) เพื่อให้ object สองตัวที่มี field เดียวกันเท่ากัน
  4. ลบการแชร์ออก: ให้เจ้าของแต่ละคนถือ copy ของตัวเอง เพราะตอนนี้ copy แยกแยะกันไม่ออก
  5. รัน 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 identityobject ต้องการ update ที่ทุก reference เห็นพร้อมกัน
ต้องการ equality ตาม content ไม่ใช่ identityobject มี lifecycle ที่ต้องจัดการ (open/close)
ต้องการใช้ใน map key หรือ setหลาย component ต้อง observe การเปลี่ยนแปลงเดียวกัน

⚠️ ไม่ควร Change Reference to Value เมื่อ:

  • หลายส่วนของระบบต้องเห็น mutation เดียวกันพร้อมกัน
  • object มีขนาดใหญ่มาก — copying ทุกครั้งแพงเกินไป
  • object เป็น entity ที่มี identity สำคัญ (เช่น user account, database record)
อะไรที่ทำให้ value object ต่างจาก reference?
bug แบบใดที่มักส่งสัญญาณว่า reference ที่แชร์กันควรกลายเป็น value?
เมื่อคุณเปลี่ยน reference ที่ mutate ได้ให้เป็น value อะไรมาแทน method ที่ mutate ในที่?
คำถามเดียวใดที่ตัดสินระหว่าง reference กับ value ได้ดีที่สุด?