Self-Encapsulate Field
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”เมื่อ class แตะต้อง field ของตัวเองโดยตรง — this.low, self.low, e.low — ทุก method ภายในเดินสายตรงเข้าไปหาที่จัดเก็บแบบดิบ ๆ Self-Encapsulate Field เดินทางแม้แต่การอ่านและเขียนภายในเหล่านั้นผ่าน method accessor ของ field เอง เมื่อทุกการเข้าถึงผ่าน getter หนึ่งตัวและ setter หนึ่งตัว คู่นั้นเพียงคู่เดียวจะกลายเป็นจุดสำหรับเพิ่มการตรวจสอบความถูกต้อง การกำหนดค่าแบบ lazy ค่าเริ่มต้น หรือ behavior ที่ subclass สามารถ override ได้
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”กลิ่นเหม็นคือ การเข้าถึง field โดยตรงภายใน class ที่เป็นเจ้าของ ในสถานการณ์ที่ hook จะช่วยได้ คุณต้องการให้ subclass คำนวณค่าด้วยวิธีต่างออกไป หรือคุณต้องการตรวจสอบความถูกต้องในทุกการกำหนดค่า หรือคุณต้องการสร้างค่าแบบ lazy ในการอ่านครั้งแรก — แต่ field ถูกแหย่จากจุดภายในนับสิบจุด จึงไม่มีรอยต่อเดียวให้แทรก logic นั้น การเข้าถึงโดยตรงนั้นดีอยู่จนกระทั่งคุณต้องการ hook ในวินาทีที่คุณต้องการ การอ่าน this.field ที่กระจัดกระจายจะมาขวางทางคุณ
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”ช่วงอุณหภูมิที่เปิดเผย width ตอนก่อน getter อ่าน field โดยตรง ดังนั้น subclass จึงไม่สามารถตีความขอบเขตใหม่ได้ ตอนหลัง code ภายในเดินผ่าน accessor และ subclass สามารถ override low() เพื่อใช้ค่าชดเชยจากการสอบเทียบ (calibration offset) ได้
// Beforeclass Range { constructor(private _low: number, private _high: number) {} width(): number { return this._high - this._low; }}
// Afterclass 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; }}# Beforeclass Range: def __init__(self, low, high): self._low = low self._high = high def width(self): return self._high - self._low
# Afterclass Range: def __init__(self, low, high): self._low = low self._high = high @property def low(self): return self._low @property def high(self): return self._high def width(self): return self.high - self.low # routed through accessors
class CalibratedRange(Range): def __init__(self, low, high, offset): super().__init__(low, high) self._offset = offset @property def low(self): return self._low + self._offset// Beforetype Range struct { low float64 high float64}func (r Range) Width() float64 { return r.high - r.low}
// After — accessor methods so a wrapping type can reinterpret a boundtype Range struct { low float64 high float64}func (r Range) Low() float64 { return r.low }func (r Range) High() float64 { return r.high }
// Width depends on the accessor, not the raw field.func WidthOf(r interface{ Low() float64; High() float64 }) float64 { return r.High() - r.Low()}
type CalibratedRange struct { Range offset float64}func (c CalibratedRange) Low() float64 { return c.Range.Low() + c.offset }// Beforestruct Range { low: f64, high: f64,}impl Range { fn width(&self) -> f64 { self.high - self.low }}
// After — a trait of accessors lets width() ignore the storagetrait Bounds { fn low(&self) -> f64; fn high(&self) -> f64; fn width(&self) -> f64 { self.high() - self.low() // routed through accessors }}
struct Range { low: f64, high: f64,}impl Bounds for Range { fn low(&self) -> f64 { self.low } fn high(&self) -> f64 { self.high }}
struct CalibratedRange { inner: Range, offset: f64,}impl Bounds for CalibratedRange { fn low(&self) -> f64 { self.inner.low() + self.offset } fn high(&self) -> f64 { self.inner.high() }}กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- สร้าง getter และ (ถ้า field ถูกเปลี่ยนค่า) setter สำหรับ field นั้น ถ้ายังไม่มีอยู่
- หาทุกที่ที่ class เจ้าของอ่าน field โดยตรง แล้วแทนที่ด้วยการเรียก getter
- หาทุกที่ที่ class เขียน field โดยตรง แล้วแทนที่ด้วยการเรียก setter
- รัน test หลังการแทนที่แต่ละครั้ง เพื่อให้ความล้มเหลวชี้ไปที่การเปลี่ยนแปลงเล็ก ๆ เพียงจุดเดียว
- ทำให้ field ดิบเป็น private เท่าที่ภาษาจะอนุญาต จะได้เหลือแค่ accessor เท่านั้นที่เข้าถึงได้
- ตอนนี้ค่อยเพิ่ม 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 การอ่าน/เขียน field | class เล็กและ simple — เพิ่ม indirection โดยไม่จำเป็น |
| ต้องการ add lazy initialization หรือ caching | field เป็น constant ที่ไม่ต้องการ control |
| validation ที่ซับซ้อนต้องทำใน setter | เพิ่ม getter/setter ที่ไม่มี logic แค่ pass through |
⚠️ ไม่ควร Self-Encapsulate Field เมื่อ:
- class ไม่มี subclass และ field ไม่ต้องการ validation
- เพิ่ม accessor เพื่อ “ความสม่ำเสมอ” แต่ไม่มี benefit จริง
- ทำให้ code อ่านยากขึ้นโดยไม่ได้ให้ flexibility ที่จำเป็น