Encapsulate Variable
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”นำข้อมูลชิ้นหนึ่งที่ caller เข้าถึงโดยตรง มาส่งทุกการเข้าถึงผ่าน function accessor คู่หนึ่ง — getter และ setter เมื่อทุกการอ่านและการเขียนไหลผ่านจุดเดียว จุดนั้นก็กลายเป็นจุดเกาะเกี่ยว (hook) คุณสามารถตรวจสอบความถูกต้องของ input บันทึก log การเปลี่ยนแปลง คำนวณค่าแบบ lazy หรือสลับวิธีแทนค่าที่อยู่เบื้องหลังทั้งหมด โดยที่ไม่มี caller คนใดต้องรับรู้
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”ตัวกระตุ้นคือ ข้อมูลที่เปลี่ยนแปลงได้และถูกใช้ร่วมกันอย่างกว้างขวาง: field หนึ่ง ตัวแปรระดับโมดูล หรือตัวแปร global ที่หลายส่วนของ code อ่านและเขียนได้ตามใจ ตราบใดที่การเข้าถึงกระจัดกระจาย คุณจะไม่สามารถเปลี่ยนอะไรเกี่ยวกับวิธีที่ข้อมูลถูกจัดเก็บหรือป้องกันได้เลย โดยไม่ต้องไล่ตามหาทุก ๆ การอ้างอิง ไม่มีรอยต่อ (seam) จุดเดียวให้เพิ่มกฎอย่าง “ค่านี้ห้ามติดลบ” หรือ “ทำให้ string เป็นมาตรฐานตอนรับเข้ามา” การห่อหุ้มจะสร้างรอยต่อนั้นขึ้นมา
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”object config ที่ field taxRate ถูกอ่านและกำหนดค่าโดยตรงทั่วทั้ง code เบส หลังจากนั้น field ดังกล่าวเป็น private อยู่หลัง accessor เพื่อให้ setter สามารถปฏิเสธค่าที่ไม่สมเหตุสมผลได้
// Beforeexport const config = { taxRate: 0.07,};// ...elsewhere, scattered and unguarded:config.taxRate = 1.5; // nobody stops thisconst r = config.taxRate;
// Afterclass Config { #taxRate = 0.07;
get taxRate(): number { return this.#taxRate; }
set taxRate(value: number) { if (value < 0 || value > 1) { throw new RangeError('taxRate must be between 0 and 1'); } this.#taxRate = value; }}
export const config = new Config();const r = config.taxRate;# Beforeclass Config: def __init__(self): self.tax_rate = 0.07# ...elsewhere, scattered and unguarded:config = Config()config.tax_rate = 1.5 # nobody stops thisr = config.tax_rate
# Afterclass Config: def __init__(self): self._tax_rate = 0.07
@property def tax_rate(self): return self._tax_rate
@tax_rate.setter def tax_rate(self, value): if not 0 <= value <= 1: raise ValueError("tax_rate must be between 0 and 1") self._tax_rate = value
config = Config()r = config.tax_rate// Beforetype Config struct { TaxRate float64 // exported, written anywhere}// ...elsewhere:cfg.TaxRate = 1.5 // nobody stops thisr := cfg.TaxRate
// Aftertype Config struct { taxRate float64 // unexported: only accessors reach it}
func (c *Config) TaxRate() float64 { return c.taxRate}
func (c *Config) SetTaxRate(value float64) error { if value < 0 || value > 1 { return fmt.Errorf("taxRate must be between 0 and 1, got %v", value) } c.taxRate = value return nil}// Beforepub struct Config { pub tax_rate: f64, // public: written anywhere}// ...elsewhere:// cfg.tax_rate = 1.5; // nobody stops this
// Afterpub struct Config { tax_rate: f64, // private: only accessors reach it}
impl Config { pub fn tax_rate(&self) -> f64 { self.tax_rate }
pub fn set_tax_rate(&mut self, value: f64) -> Result<(), String> { if !(0.0..=1.0).contains(&value) { return Err(format!("tax_rate must be between 0 and 1, got {value}")); } self.tax_rate = value; Ok(()) }}flowchart LR
subgraph Before["Before"]
A["caller A"] --> D[("data field<br/>read & written directly")]
B["caller B"] --> D
C["caller C"] --> D
end
subgraph After["After"]
A2["caller A"] --> G["get() / set()"]
B2["caller B"] --> G
C2["caller C"] --> G
G --> D2[("data field")]
end
Before -.->|"Encapsulate Variable"| After กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- สร้าง getter และ setter (หรือ accessor function) ที่อ่านและเขียนตัวแปร ตอนแรกให้ทำแค่ return กับ assign เท่านั้น ยังไม่ต้องมีอย่างอื่น
- หาทุก ๆ การอ้างอิงถึงตัวแปรดิบ แทนที่การอ่านแต่ละครั้งด้วยการเรียก getter และการเขียนแต่ละครั้งด้วยการเรียก setter ทำทีละกลุ่มเล็ก ๆ
- รัน test หลังจากแต่ละกลุ่ม
- จำกัดการเข้าถึงตัวแปรเอง — ทำให้ field เป็น private, unexported หรือเข้าถึงไม่ได้จากภายนอกด้วยวิธีอื่น — เพื่อให้ accessor เป็นทางเข้าทางเดียว
- รัน test อีกครั้ง ไม่ควรมีอะไรพัง เพราะทุกจุดผ่าน accessor อยู่แล้ว
- ตอนนี้คุณมีรอยต่อแล้ว เพิ่มการตรวจสอบความถูกต้อง การ log หรือการคำนวณแบบ lazy ภายใน accessor ในฐานะการเปลี่ยนแปลงที่แยกออกมาและตั้งใจทำ
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”หยิบ Encapsulate Variable มาใช้เมื่อข้อมูลแก้ค่าได้และถูกใช้ร่วมกัน เมื่อคุณอยากเพิ่มกฎว่าค่าไหนถูกต้อง หรือเมื่อสงสัยว่าวิธีเก็บข้อมูลอาจเปลี่ยนในอนาคต ท่านี้เป็นเงื่อนไขตั้งต้นของอีกหลายท่าที่ตามมา เพราะคุณสลับวิธีเก็บข้อมูลอย่างปลอดภัยไม่ได้เลย จนกว่าทุกการเข้าถึงจะไหลผ่านจุดเดียว
ต้นทุนคือชั้นการอ้อมเพิ่มมาหนึ่งชั้น กับพิธีเล็ก ๆ น้อย ๆ ถ้าเป็นค่าที่ local จริง ๆ ค่าที่ immutable หรือค่าที่ใช้ครั้งเดียว พิธีนั้นไม่ช่วยอะไร ปล่อยเปลือยไว้ดีกว่า การห่อหุ้มจะคุ้มก็ต่อเมื่อข้อมูลถูกใช้ร่วมกันและอยู่ยาวเท่านั้น ส่วน Encapsulate Collection ที่เกี่ยวข้องกัน เอาแนวคิดเดียวกันไปใช้กับ field ประเภท list และ map ซึ่งการเปิดเผย container ดิบ ๆ อันตรายเป็นพิเศษ
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Encapsulate Variable เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| variable ถูก access จากหลายที่และต้องการ control | variable เป็น constant ที่ไม่เปลี่ยน |
| ต้องการ add validation หรือ side effect เมื่อ set | overhead ไม่คุ้มสำหรับ variable ที่ใช้ภายใน scope เล็ก |
| ต้องการ track การเปลี่ยนแปลง (debug, logging) | เป็น private field ใน class ที่ access จาก method เดียว |
⚠️ ไม่ควร Encapsulate Variable เมื่อ:
- variable เป็น local ใน function — ไม่มีใคร access จากภายนอก
- ทำเพื่อ “best practice” โดยไม่มีเหตุผลที่ชัดเจน
- เพิ่ม getter/setter ที่ไม่มี logic — แค่ pass through