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

Encapsulate Variable

นำข้อมูลชิ้นหนึ่งที่ caller เข้าถึงโดยตรง มาส่งทุกการเข้าถึงผ่าน function accessor คู่หนึ่ง — getter และ setter เมื่อทุกการอ่านและการเขียนไหลผ่านจุดเดียว จุดนั้นก็กลายเป็นจุดเกาะเกี่ยว (hook) คุณสามารถตรวจสอบความถูกต้องของ input บันทึก log การเปลี่ยนแปลง คำนวณค่าแบบ lazy หรือสลับวิธีแทนค่าที่อยู่เบื้องหลังทั้งหมด โดยที่ไม่มี caller คนใดต้องรับรู้

ตัวกระตุ้นคือ ข้อมูลที่เปลี่ยนแปลงได้และถูกใช้ร่วมกันอย่างกว้างขวาง: field หนึ่ง ตัวแปรระดับโมดูล หรือตัวแปร global ที่หลายส่วนของ code อ่านและเขียนได้ตามใจ ตราบใดที่การเข้าถึงกระจัดกระจาย คุณจะไม่สามารถเปลี่ยนอะไรเกี่ยวกับวิธีที่ข้อมูลถูกจัดเก็บหรือป้องกันได้เลย โดยไม่ต้องไล่ตามหาทุก ๆ การอ้างอิง ไม่มีรอยต่อ (seam) จุดเดียวให้เพิ่มกฎอย่าง “ค่านี้ห้ามติดลบ” หรือ “ทำให้ string เป็นมาตรฐานตอนรับเข้ามา” การห่อหุ้มจะสร้างรอยต่อนั้นขึ้นมา

object config ที่ field taxRate ถูกอ่านและกำหนดค่าโดยตรงทั่วทั้ง code เบส หลังจากนั้น field ดังกล่าวเป็น private อยู่หลัง accessor เพื่อให้ setter สามารถปฏิเสธค่าที่ไม่สมเหตุสมผลได้

// Before
export const config = {
taxRate: 0.07,
};
// ...elsewhere, scattered and unguarded:
config.taxRate = 1.5; // nobody stops this
const r = config.taxRate;
// After
class 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;
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
การเข้าถึงโดยตรงกลายเป็นการเข้าถึงผ่าน accessor คู่เดียว
  1. สร้าง getter และ setter (หรือ accessor function) ที่อ่านและเขียนตัวแปร ตอนแรกให้ทำแค่ return กับ assign เท่านั้น ยังไม่ต้องมีอย่างอื่น
  2. หาทุก ๆ การอ้างอิงถึงตัวแปรดิบ แทนที่การอ่านแต่ละครั้งด้วยการเรียก getter และการเขียนแต่ละครั้งด้วยการเรียก setter ทำทีละกลุ่มเล็ก ๆ
  3. รัน test หลังจากแต่ละกลุ่ม
  4. จำกัดการเข้าถึงตัวแปรเอง — ทำให้ field เป็น private, unexported หรือเข้าถึงไม่ได้จากภายนอกด้วยวิธีอื่น — เพื่อให้ accessor เป็นทางเข้าทางเดียว
  5. รัน test อีกครั้ง ไม่ควรมีอะไรพัง เพราะทุกจุดผ่าน accessor อยู่แล้ว
  6. ตอนนี้คุณมีรอยต่อแล้ว เพิ่มการตรวจสอบความถูกต้อง การ log หรือการคำนวณแบบ lazy ภายใน accessor ในฐานะการเปลี่ยนแปลงที่แยกออกมาและตั้งใจทำ

หยิบ Encapsulate Variable มาใช้เมื่อข้อมูลแก้ค่าได้และถูกใช้ร่วมกัน เมื่อคุณอยากเพิ่มกฎว่าค่าไหนถูกต้อง หรือเมื่อสงสัยว่าวิธีเก็บข้อมูลอาจเปลี่ยนในอนาคต ท่านี้เป็นเงื่อนไขตั้งต้นของอีกหลายท่าที่ตามมา เพราะคุณสลับวิธีเก็บข้อมูลอย่างปลอดภัยไม่ได้เลย จนกว่าทุกการเข้าถึงจะไหลผ่านจุดเดียว

ต้นทุนคือชั้นการอ้อมเพิ่มมาหนึ่งชั้น กับพิธีเล็ก ๆ น้อย ๆ ถ้าเป็นค่าที่ local จริง ๆ ค่าที่ immutable หรือค่าที่ใช้ครั้งเดียว พิธีนั้นไม่ช่วยอะไร ปล่อยเปลือยไว้ดีกว่า การห่อหุ้มจะคุ้มก็ต่อเมื่อข้อมูลถูกใช้ร่วมกันและอยู่ยาวเท่านั้น ส่วน Encapsulate Collection ที่เกี่ยวข้องกัน เอาแนวคิดเดียวกันไปใช้กับ field ประเภท list และ map ซึ่งการเปิดเผย container ดิบ ๆ อันตรายเป็นพิเศษ

ใช้ Encapsulate Variable เมื่อหลีกเลี่ยงเมื่อ
variable ถูก access จากหลายที่และต้องการ controlvariable เป็น constant ที่ไม่เปลี่ยน
ต้องการ add validation หรือ side effect เมื่อ setoverhead ไม่คุ้มสำหรับ variable ที่ใช้ภายใน scope เล็ก
ต้องการ track การเปลี่ยนแปลง (debug, logging)เป็น private field ใน class ที่ access จาก method เดียว

⚠️ ไม่ควร Encapsulate Variable เมื่อ:

  • variable เป็น local ใน function — ไม่มีใคร access จากภายนอก
  • ทำเพื่อ “best practice” โดยไม่มีเหตุผลที่ชัดเจน
  • เพิ่ม getter/setter ที่ไม่มี logic — แค่ pass through
เป้าหมายหลักของ Encapsulate Variable คืออะไร?
หลังจากแนะนำ accessor เข้ามาแล้ว ขั้นตอนต่อไปที่สำคัญคืออะไร?
ข้อมูลชนิดใดที่สมควรใช้ Encapsulate Variable มากที่สุด?
รอยต่อการเข้าถึงจุดเดียวเปิดทางให้ทำอะไรได้อย่างเป็นประโยชน์ที่สุด?