Encapsulation
object ควรจัดการสถานะและพฤติกรรมของตัวเอง
Encapsulation (การห่อหุ้ม) หมายถึงแนวคิดที่ว่า object ควรจัดการพฤติกรรมและสถานะ (state) ของตัวเอง เพื่อให้ผู้ร่วมงานของมันไม่ต้องกังวลกับการทำงานภายในของ object นั้น มันคือหนึ่งในสี่เสาหลักดั้งเดิมของ object-oriented programming (ร่วมกับ abstraction, inheritance และ polymorphism) และมักถูกมองว่าเป็นหลักการพื้นฐานที่สุดในสี่ข้อนี้ เพราะการออกแบบเชิงวัตถุที่ดีแทบทั้งหมดพึ่งพา encapsulation เป็นฐานราก
แนวคิดหลัก
หัวข้อที่มีชื่อว่า “แนวคิดหลัก”Encapsulation มีสองความหมายที่เกี่ยวโยงกันแต่แยกจากกันได้:
- การรวมข้อมูลกับพฤติกรรม (bundling) — จัดกลุ่มข้อมูล (state) เข้ากับ method ที่ทำงานบนข้อมูลนั้นให้อยู่ในหน่วยเดียวกัน (class) แทนที่จะกระจายข้อมูลกับ logic ที่ประมวลผลมันไว้คนละที่
- การจำกัดการเข้าถึง (access restriction) — ใช้กลไกของภาษาเพื่อซ่อนรายละเอียดภายในของ object ไม่ให้ code ภายนอกเข้าถึงหรือแก้ไขได้โดยตรง
ภาษาเชิงวัตถุมี built-in support สำหรับควบคุมการมองเห็น (visibility) ของโครงสร้างระดับ class เช่น public, private, protected, internal และนักพัฒนาควรใช้เครื่องมือเหล่านี้เพื่อแยกระหว่าง interface สาธารณะและไม่สาธารณะของ object การไม่ใช้หลัก encapsulation อย่างเหมาะสมนำไปสู่ code smell และปัญหาการออกแบบที่เกี่ยวข้องมากมาย เช่น การละเมิด Don’t Repeat Yourself, Tell Don’t Ask และ Flags Over Objects
โดยอุดมคติ object ควรถูกรักษาให้อยู่ในสถานะที่ถูกต้อง (valid state) เสมอ โดยควบคุมวิธีที่สถานะถูกแก้ไขเพื่อหลีกเลี่ยงการตกไปอยู่ในสถานะที่ไม่สมเหตุสมผล ตัวอย่างเช่น class ที่แทนสินค้าอาจมีสถานะแทนปริมาตร (สมมติเป็นลิตร) ปริมาตรนี้อาจแทนด้วยชนิดตัวเลข เช่น double แต่ไม่มีทางสมเหตุสมผลที่ค่าจะติดลบ หาก class เปิดเผยข้อมูลเป็น public field code ใด ๆ ในระบบก็สามารถตั้งค่าให้ติดลบได้:
public field เปิดช่องให้ตั้งค่าที่ไม่ถูกต้องได้
public class Product{ public double Volume;}ภาษาเชิงวัตถุสมัยใหม่ส่วนใหญ่รองรับแนวคิด Property นักพัฒนาหลายคนจึงปรับปรุงเล็กน้อยด้วยการใช้ property:
ใช้ property แต่ยังไม่รับประกันค่าที่ถูกต้อง
public class Product{ public double Volume { get; set; }}อย่างไรก็ตาม นี่ยังไม่รับประกันว่าปริมาตรของ Product จะเป็นบวก จะทำได้ก็ต่อเมื่อมีการใส่ private backing field เข้ามาอย่างชัดเจนเท่านั้น:
private backing field บังคับ invariant ให้ค่าเป็นบวก
public class Product{ private double _volume; public double Volume { get { return _volume; } set { if (value < 0) { throw new ArgumentOutOfRangeException("Volume must be non-negative."); } _volume = value; } }}การออกแบบนี้ดีขึ้น แต่ยังมีกลิ่นของ code smell แบบ Primitive Obsession อยู่บ้าง — ทำไมการรู้ว่าสถานะใดถูกต้องสำหรับ Volume จึงเป็นความรับผิดชอบของ class Product? ปริมาตรในโลกเรขาคณิตแบบยุคลิดไม่มีทางติดลบในบริบทใด ๆ ไม่ใช่แค่สำหรับ Product เท่านั้น จึงอาจสมเหตุสมผลที่จะแทน Volume เป็นชนิดของมันเอง น่าจะเป็น Value Object ซึ่งมีพฤติกรรมของตัวเอง (อย่างน้อยคือวิธีบังคับขอบเขตค่าที่ถูกต้อง แต่คงมีรายละเอียดสำคัญอื่นด้วย เช่น หน่วย):
แยก Volume เป็น Value Object ที่ดูแลความถูกต้องของตัวเอง
public class Volume{ private Volume(double amount, string unitOfMeasure) { if(amount < 0) throw new ArgumentOutOfRangeException("Volume amount must be non-negative."); Amount = amount; UnitOfMeasure = unitOfMeasure; } public static Volume InLiters(double amount) { return new Volume(amount, "Liters"); } // เพิ่ม static factory method อื่น ๆ สำหรับหน่วยอื่นตรงนี้ public double Amount { get; private set; } public string UnitOfMeasure { get; private set; }
// อาจเพิ่ม method สำหรับแปลงหน่วยจากหน่วยหนึ่งไปอีกหน่วยตรงนี้}ณ จุดนี้ Product ไม่ต้องกังวลอีกว่า Volume อยู่ในสถานะบวกหรือไม่ จึงทำให้เรียบง่ายลงได้ อย่างไรก็ตาม มันก็ยังไม่ควรเปิดเผยสถานะภายในโดยตรง เพราะเป็นไปได้ว่า Product จะอยากตอบสนองต่อการเปลี่ยนแปลงใด ๆ ที่เกิดกับสถานะของมัน
Martin Fowler เสริมมุมมองที่สำคัญไว้ใน Getter Eradicator: เป้าหมายที่แท้จริงของ encapsulation ไม่ใช่การซ่อนข้อมูลเฉย ๆ แต่คือการซ่อน การตัดสินใจในการออกแบบ (design decisions) ที่อาจเปลี่ยนแปลงได้ในอนาคต — คำถามที่ควรถามเสมอคือ “เรากำลังซ่อนความผันแปร (variability) อะไรอยู่ และทำไม” มุมมองนี้ขยายจากการซ่อน field ธรรมดาไปถึงการซ่อน protocol การสื่อสารและการตัดสินใจเชิงสถาปัตยกรรมอื่น ๆ ด้วย
ทำไมถึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมถึงสำคัญ”- ป้องกัน invalid state — เมื่อ object ควบคุมวิธีที่สถานะของตัวเองถูกแก้ไข มันสามารถบังคับ invariant (กฎที่ต้องเป็นจริงเสมอ) ได้ ป้องกันไม่ให้ระบบตกไปอยู่ในสถานะที่ไม่สมเหตุสมผล
- ลด coupling — เมื่อ code ภายนอกพึ่งพาเฉพาะ public interface ไม่ใช่รายละเอียดภายใน การเปลี่ยนแปลงการ implement ภายในจึงไม่กระทบผู้เรียกใช้ ทำให้ระบบยืดหยุ่นต่อการเปลี่ยนแปลง
- รองรับการเปลี่ยนแปลงในอนาคต — ตามที่ Fowler ชี้ การซ่อน “การตัดสินใจที่อาจเปลี่ยน” ไว้หลัง interface ที่มั่นคง ทำให้ทีมปรับปรุง implementation ได้โดยไม่ต้องแก้ code ทุกจุดที่เรียกใช้
- จัดสรร behavior ให้ถูกที่ — encapsulation ที่ดีผลักดันให้ logic ที่เกี่ยวกับข้อมูลหนึ่ง ๆ อยู่ใกล้ข้อมูลนั้น ตาม Tell Don’t Ask แทนที่จะดึงข้อมูลออกมาประมวลผลข้างนอก ซึ่งนำไปสู่ Data Class ที่ Fowler มองว่าเป็น code smell
- เป็นฐานของหลักการอื่น — การละเมิด encapsulation มักลากไปสู่การละเมิดหลักการอื่นตามมา เช่น DRY, Tell Don’t Ask, Make Illegal States Unrepresentable และ Law of Demeter
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”การเปิดเผย property โดยตรงผ่าน getter/setter ที่ไม่มีการตรวจสอบ และการเปิดเผย property ที่เป็น collection (แม้ไม่มี setter) เป็นสองการละเมิด encapsulation ที่พบบ่อยที่สุด ลองดูตัวอย่าง collection เพิ่มเติม:
ละเมิด encapsulation — collection ภายในถูกเปิดเผยโดยตรง
public class Order{ public List<OrderLine> Lines { get; } = new();
public decimal Total => Lines.Sum(l => l.Price * l.Quantity);}
// code ภายนอกสามารถทำลาย invariant ของ Order ได้ตรง ๆorder.Lines.Clear();order.Lines.Add(new OrderLine(product, -5)); // จำนวนติดลบ ไม่มีใครห้ามได้แม้ Lines จะมี getter อย่างเดียว (ไม่มี setter) แต่เพราะมันคืนค่า List<OrderLine> ตรง ๆ code ภายนอกจึงยังเรียก .Add(), .Clear(), .Remove() บน collection นั้นได้โดยตรง ทำให้ Order ไม่มีทางรู้หรือควบคุมการเปลี่ยนแปลงเลย — Fowler เรียกปัญหานี้ว่า Encapsulated Collection ที่ยังไม่สมบูรณ์
รักษา encapsulation — เปิดเผยเฉพาะ read-only view และควบคุมการแก้ไขผ่าน method
public class Order{ private readonly List<OrderLine> _lines = new();
public IReadOnlyList<OrderLine> Lines => _lines;
public decimal Total => _lines.Sum(l => l.Price * l.Quantity);
public void AddLine(Product product, int quantity) { if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity), "Quantity must be positive.");
_lines.Add(new OrderLine(product, quantity)); }
public void RemoveLine(OrderLine line) { _lines.Remove(line); }}ตอนนี้ Order เป็นเจ้าของ invariant ของตัวเองอย่างสมบูรณ์: field ภายในเป็น private, มุมมองที่เปิดเผยออกไปเป็น IReadOnlyList<T> ที่แก้ไขไม่ได้จากภายนอก และการแก้ไขทุกครั้งต้องผ่าน method AddLine/RemoveLine ที่ตรวจสอบความถูกต้องก่อนเสมอ — สอดคล้องกับหลัก Tell Don’t Ask ที่บอกให้ object ทำงานให้เรา แทนที่จะดึงข้อมูลออกมาทำเอง
สัญญาณว่ากำลังละเมิดหลักการนี้
หัวข้อที่มีชื่อว่า “สัญญาณว่ากำลังละเมิดหลักการนี้”- Public field ที่เก็บ state โดยไม่มีการควบคุมใด ๆ
- Property ที่มี
{ get; set; }แบบเปิดกว้าง โดยไม่มีการตรวจสอบค่าที่รับเข้ามา - collection ที่เปิดเผยเป็น
List<T>แทนที่จะเป็นIReadOnlyList<T>หรือ method ที่ควบคุมการเพิ่ม/ลบ - code เรียกหลาย getter ของ object เดียวกันติดกันเพื่อไปคำนวณ logic ข้างนอก (สัญญาณของการละเมิด Tell Don’t Ask และมักมาคู่กับการละเมิด Law of Demeter)
- class ที่มีแต่ field/property กับ getter-setter ล้วน ไม่มี behavior เลย (Data Class / Anemic Domain Model)
- Object ที่สามารถตกไปอยู่ในสถานะที่ไม่สมเหตุสมผลได้ (เช่น
Volumeติดลบ,EndDateมาก่อนStartDate) เพราะไม่มีจุดใดบังคับ invariant ไว้เลย - ใช้ constant แบบ magic string/number ที่ควรถูกห่อหุ้มเป็น Value Object แทน (อาการของ Primitive Obsession)
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Tell Don’t Ask
- Don’t Repeat Yourself
- Make Illegal States Unrepresentable
- Law of Demeter
- Primitive Obsession
- Feature Envy