Ensuring Integrity of State
คำตอบและหมุดเชื่อมโยง: คำว่า "State Integrity" หมายถึง ความถูกต้อง สมบูรณ์ และสอดคล้องกับกฎทางธุรกิจของสถานะภายในของวัตถุ (Internal State) ตั้งแต่วินาทีแรกที่วัตถุถือกำเนิดขึ้น ไปจนถึงทุกๆ การเปลี่ยนสถานะตลอดช่วงชีวิตของมัน
ผู้เขียนตั้งชื่อบทนี้ว่า Ensuring integrity of state เพื่อเตือนสติว่า ช่องโหว่ความปลอดภัยส่วนใหญ่ไม่ได้เกิดจากการถูกเจาะผ่านเครือข่าย แต่เกิดจากการที่เราปล่อยให้วัตถุในระบบ "ถูกสร้างขึ้นแบบพิการ (Partially Initialized)" หรือเปิด Unrestricted Setters ทิ้งไว้ให้โค้ดภายนอกเข้ามาแอบแก้ไขสถานะได้ตามใจชอบ การปกป้อง State Integrity จึงเป็นปราการสำคัญในการป้องกันไม่ให้ระบบตกอยู่ในสถานะที่ผิดปกติหรือถูกชักจูงโดยแฮกเกอร์!
Day 1: CIA-T / Day 2: The Anti-Hamlet / Day 3: DDD Core / Day 4: Immutability & Validation Pipeline / Day 5: Domain Primitives
Day 6 จาก 14 วัน — สวมความปลอดภัยให้แก่ Entities & Aggregate Roots ที่มีการเปลี่ยนแปลงสถานะ
สร้างความแข็งแกร่งด้วย Consistent on Creation, Intent-Revealing Operations, Guarded Collections และ Self-Destructing Builders
เรื่องราวและมโนทัศน์เชิงลึก (Narrative Scenarios & Architectural Shields)
📖 เรื่องราวจริงในระบบ e-Commerce
ระบบสั่งซื้อสินค้าเกิดปัญหารายการสั่งซื้อค้างนับพัน และระบบคลังสินค้าล่มอย่างไม่ทราบสาเหตุ เมื่อสืบสวนพบว่า โปรแกรมเมอร์ออกแบบคลาส Order ด้วย No-Arg Constructor และเรียก Setter ทีละบรรทัด:
Order order = new Order(); order.setCustomerId(customerId); // เกิด Network Timeout หรือ Exception กลางคันก่อน setShippingAddress()! orderService.save(order); // วัตถุพิการถูกบันทึกลง Database!
Order ถูกบันทึกลงฐานข้อมูลทั้งที่ ShippingAddress = null เมื่อคลังสินค้าดึงไปรัน เกิด NullPointerException เซิร์ฟเวอร์ค้าง และคำสั่งซื้อติดค้างเป็นลูกโซ่!
🧠 ถอดรหัสสู่โลกซอฟต์แวร์ (Mental Model)
📖 เรื่องราวจริงในคลาส BankAccount
ในคลาส BankAccount โปรแกรมเมอร์สร้าง Private Field private double balance; แต่เปิดเมธอด public void setBalance(double balance) เอาไว้ตามความเคยชินของ JavaBeans
account.setBalance(1000000.0) ได้โดยตรง! การเสกเงินหนึ่งล้านบาทนี้ไม่ต้องผ่านขั้นตอน Deposit, ไม่ผ่านการตรวจสอบสิทธิ์, และไม่มีการบันทึก Audit Log เลยแม้แต่บรรทัดเดียว!
🧠 ถอดรหัสสู่โลกซอฟต์แวร์ (Mental Model)
deposit(Money amount) หรือ withdraw(Money amount) ซึ่งบังคับตรวจ Invariants และ Audit Trail ภายในตัวเองเสมอ
📖 รอยรั่วของ Getter ที่ส่งออก Collection ฉบับจริง
ในคลาส Order (Aggregate Root) มีการเก็บรายการสินค้า private List<OrderLine> items; และสร้าง Getter คืนค่าอ้างอิงตรงๆ:
public List<OrderLine> getItems() { return this.items; // Danger: Escaping Reference! }
order.getItems().clear() ลบรายการสินค้าทิ้ง หรือสั่ง order.getItems().add(fakeItem) แอบยัดสินค้าขยะเข้ามา โดยที่ Aggregate Root อย่าง Order ไม่รู้เห็นเลยแม้แต่น้อย!
🧠 ถอดรหัสสู่โลกซอฟต์แวร์ (Mental Model)
Collections.unmodifiableList(items) เพื่อบล็อกคำสั่ง add/remove/clear จากภายนอก และทำ Defensive Copying ใน Constructor เพื่อตัดการเชื่อมโยงกับ List ภายนอก!
ส่งค่า Mandatory Fields ทั้งหมดผ่าน Constructor ล็อคฟิลด์ที่ไม่เปลี่ยนเป็น private final เพื่อขจัด No-Arg Constructor และตัดวงจรอ็อบเจกต์พิการ
เปลี่ยนจาก setPaid(true) หรือ setStatus("CANCELLED") เป็นเมธอดสื่อเจตนา เช่น markPaid(), cancel() ที่มี Precondition เช็กสถานะก่อนรัน
รวบรวมเงื่อนไขข้ามหลายฟิลด์ (เช่น บัญชีต้องมี Credit Limit หรือ Fallback Account อย่างใดอย่างหนึ่งเท่านั้น — XOR) ไว้ใน checkInvariants() และตรวจตอนจบทุกเมธอด
สำหรับ Entity ที่ซับซ้อน ใช้ Builder ซ่อนอ็อบเจกต์ที่ยังสร้างไม่เสร็จไว้ ตรวจ checkInvariants() ในเมธอด build() แล้ว Self-destruct (account = null) ทันที
แบบฝึกหัดปฏิบัติตามสถานการณ์จริง (Hands-on Refactoring)
// โค้ดเดิม: สร้างวัตถุพิการได้, เปิด Setters อันตราย, และส่ง Collection ฉบับจริงออกไป public class Order { private Long id; private CustomerId customerId; private Address shippingAddress; private List<OrderLine> items = new ArrayList<>(); private String status; // ช่องโหว่ 1: No-arg constructor อนุญาตให้สร้างวัตถุพิการที่ยังไม่มีข้อมูล! public Order() {} public void setCustomerId(CustomerId customerId) { this.customerId = customerId; } public void setShippingAddress(Address address) { this.shippingAddress = address; } // ช่องโหว่ 2: Unrestricted Setter แอบเปลี่ยนสถานะข้ามกฎธุรกิจได้ public void setStatus(String status) { this.status = status; } // ช่องโหว่ 3: Escaping reference! ภายนอกสั่ง clear() หรือ add() ได้เอง! public List<OrderLine> getItems() { return this.items; } }
order.getItems().clear() ล้างสินค้าทิ้งได้เอง, และเปลี่ยนสถานะโดยไม่ผ่านกฎทางธุรกิจ
import static org.apache.commons.lang3.Validate.*; import java.util.Collections; import java.util.List; import java.util.ArrayList; public final class Order { private final OrderId id; // Partially Immutable: ห้ามแก้หลังสร้าง private final CustomerId customerId; private final Address shippingAddress; private final List<OrderLine> items; private OrderStatus status; // 1. Mandatory Constructor: ข้อมูลครบถ้วนแต่วินาทีแรก (Consistent on Creation) public Order(OrderId id, CustomerId customerId, Address shippingAddress, List<OrderLine> initialItems) { this.id = notNull(id, "OrderId is required"); this.customerId = notNull(customerId, "CustomerId is required"); this.shippingAddress = notNull(shippingAddress, "ShippingAddress is required"); notNull(initialItems, "Initial items required"); isTrue(!initialItems.isEmpty(), "Order must contain at least one item"); // Defensive Copy เพื่อตัดการเชื่อมโยงกับ List ภายนอก this.items = new ArrayList<>(initialItems); this.status = OrderStatus.CREATED; } // 2. Intent-Revealing Domain Operations แทน Unrestricted Setters public void cancel() { validState(this.status == OrderStatus.CREATED, "Cannot cancel an order that is already processed"); this.status = OrderStatus.CANCELLED; } public void addOrderLine(OrderLine item) { notNull(item, "Item cannot be null"); validState(this.status == OrderStatus.CREATED, "Cannot add items to a finalized order"); this.items.add(item); } // 3. Guarded Collections: ส่งออกเฉพาะ Unmodifiable List ป้องกัน Escaping Reference public List<OrderLine> items() { return Collections.unmodifiableList(items); } }
cancel() ที่ตรวจสอบกฎธุรกิจ, และป้องกัน Collection ด้วย unmodifiableList ทำให้ภายนอกแอบแก้ไขข้อมูลไม่ได้ 100%!
ชุดควิซประเมินความรู้ประจำวัน
ตอบปุ๊บ รู้ผลเฉลยทันที พร้อมคำอธิบายและหลอดพลังงาน Energy Charge
ทดสอบความรู้ Day 6 สำเร็จแล้ว!
สรุปการเชื่อมโยงกลับสู่ Chapter 6 — Ensuring integrity of state
เรื่องราวของโศกนาฏกรรมสั่งซื้อสินค้าไร้ที่อยู่จัดส่ง, อันตรายของ Unrestricted Setters, และการป้องกัน Escaping Reference ใน Collection ทั้งหมดนี้ ล้วนเป็นคำตอบที่เด่นชัดของหัวข้อบท "Ensuring integrity of state" ว่า การรักษาความถูกต้องของสถานะไม่ใช่เรื่องของการติดแอนตี้ไวรัส แต่คือการออกแบบ Entity ให้ Consistent on Creation, ปิดกั้นการแอบแก้ไขสถานะจากภายนอกด้วย Intent-Revealing Domain Operations, และคุ้มกันข้อมูลภายในด้วย Guarded Collections เพื่อให้ระบบของเราทำงานได้อย่างถูกต้องและปลอดภัยในทุกสถานการณ์ครับ!