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

Self-Encapsulate Field

เมื่อ class แตะต้อง field ของตัวเองโดยตรง — this.low, self.low, e.low — ทุก method ภายในเดินสายตรงเข้าไปหาที่จัดเก็บแบบดิบ ๆ Self-Encapsulate Field เดินทางแม้แต่การอ่านและเขียนภายในเหล่านั้นผ่าน method accessor ของ field เอง เมื่อทุกการเข้าถึงผ่าน getter หนึ่งตัวและ setter หนึ่งตัว คู่นั้นเพียงคู่เดียวจะกลายเป็นจุดสำหรับเพิ่มการตรวจสอบความถูกต้อง การกำหนดค่าแบบ lazy ค่าเริ่มต้น หรือ behavior ที่ subclass สามารถ override ได้

กลิ่นเหม็นคือ การเข้าถึง field โดยตรงภายใน class ที่เป็นเจ้าของ ในสถานการณ์ที่ hook จะช่วยได้ คุณต้องการให้ subclass คำนวณค่าด้วยวิธีต่างออกไป หรือคุณต้องการตรวจสอบความถูกต้องในทุกการกำหนดค่า หรือคุณต้องการสร้างค่าแบบ lazy ในการอ่านครั้งแรก — แต่ field ถูกแหย่จากจุดภายในนับสิบจุด จึงไม่มีรอยต่อเดียวให้แทรก logic นั้น การเข้าถึงโดยตรงนั้นดีอยู่จนกระทั่งคุณต้องการ hook ในวินาทีที่คุณต้องการ การอ่าน this.field ที่กระจัดกระจายจะมาขวางทางคุณ

ช่วงอุณหภูมิที่เปิดเผย width ตอนก่อน getter อ่าน field โดยตรง ดังนั้น subclass จึงไม่สามารถตีความขอบเขตใหม่ได้ ตอนหลัง code ภายในเดินผ่าน accessor และ subclass สามารถ override low() เพื่อใช้ค่าชดเชยจากการสอบเทียบ (calibration offset) ได้

// Before
class Range {
constructor(private _low: number, private _high: number) {}
width(): number {
return this._high - this._low;
}
}
// After
class Range {
constructor(private _low: number, private _high: number) {}
get low(): number { return this._low; }
get high(): number { return this._high; }
set low(v: number) { this._low = v; }
set high(v: number) { this._high = v; }
width(): number {
return this.high - this.low; // routed through accessors
}
}
class CalibratedRange extends Range {
constructor(low: number, high: number, private offset: number) {
super(low, high);
}
get low(): number { return super.low + this.offset; }
}
  1. สร้าง getter และ (ถ้า field ถูกเปลี่ยนค่า) setter สำหรับ field นั้น ถ้ายังไม่มีอยู่
  2. หาทุกที่ที่ class เจ้าของอ่าน field โดยตรง แล้วแทนที่ด้วยการเรียก getter
  3. หาทุกที่ที่ class เขียน field โดยตรง แล้วแทนที่ด้วยการเรียก setter
  4. รัน test หลังการแทนที่แต่ละครั้ง เพื่อให้ความล้มเหลวชี้ไปที่การเปลี่ยนแปลงเล็ก ๆ เพียงจุดเดียว
  5. ทำให้ field ดิบเป็น private เท่าที่ภาษาจะอนุญาต จะได้เหลือแค่ accessor เท่านั้นที่เข้าถึงได้
  6. ตอนนี้ค่อยเพิ่ม hook ที่ต้องการ เช่น validate ใน setter ค่าเริ่มต้นหรือการสร้างแบบ lazy ใน getter หรือการ override ใน subclass โดยมั่นใจได้ว่าทุกการเข้าถึงไหลผ่านจุดเดียวกันหมด

หยิบ Self-Encapsulate Field มาใช้เมื่อคุณอยากให้ subclass ตีความค่าใหม่ได้ เมื่อคุณต้องการจุดเดียวที่การันตีเรื่อง validate หรือการกำหนดค่าแบบ lazy หรือใช้เป็นขั้นเตรียมพร้อมก่อนเปลี่ยนวิธีเก็บ field ท่านี้เข้าคู่กับ Replace Type Code with Subclasses ได้อย่างเป็นธรรมชาติ เพราะ subclass ต้อง override วิธีสร้างค่านั้นได้

ต้นทุนคือการอ้อม (indirection) และพิธีการเล็กน้อย — การเรียก getter แทนการอ่าน field เปล่า ๆ หลายทีมชอบการเข้าถึงโดยตรงจนกว่าความจำเป็นที่เป็นรูปธรรมสำหรับ hook จะปรากฏ แล้วจึงใช้ท่านี้ ใน Go และ Rust ที่ไม่มีการสืบทอด สิ่งที่เทียบเท่าคือการนิยาม method accessor (มักอยู่หลัง interface หรือ trait) เพื่อให้ type ที่ห่อหุ้มสามารถซ้อน behavior ของตัวเองทับลงไปได้

ใช้ Self-Encapsulate Field เมื่อหลีกเลี่ยงเมื่อ
subclass ต้องการ override การอ่าน/เขียน fieldclass เล็กและ simple — เพิ่ม indirection โดยไม่จำเป็น
ต้องการ add lazy initialization หรือ cachingfield เป็น constant ที่ไม่ต้องการ control
validation ที่ซับซ้อนต้องทำใน setterเพิ่ม getter/setter ที่ไม่มี logic แค่ pass through

⚠️ ไม่ควร Self-Encapsulate Field เมื่อ:

  • class ไม่มี subclass และ field ไม่ต้องการ validation
  • เพิ่ม accessor เพื่อ “ความสม่ำเสมอ” แต่ไม่มี benefit จริง
  • ทำให้ code อ่านยากขึ้นโดยไม่ได้ให้ flexibility ที่จำเป็น
Self-Encapsulate Field เดินทางอะไรผ่าน accessor ที่ Encapsulate Variable ไม่ทำ?
ทำไมคู่ getter/setter เพียงคู่เดียวจึงมีค่า?
ใน Go หรือ Rust ที่ไม่มีการสืบทอด อะไรทำหน้าที่เป็น accessor ที่ override ได้?
Self-Encapsulate Field เตรียมการสำหรับท่าใดได้อย่างเป็นธรรมชาติที่สุด?