Reducing Complexity of State
คำตอบและหมุดเชื่อมโยง: คำว่า "Reducing complexity of state" ตั้งขึ้นเพื่อแก้ปัญหาที่เกิดขึ้นเมื่อ Entity ในระบบเริ่มเติบโต มีหลายสถานะ (States) และมีเงื่อนไขการเปลี่ยนสถานะ (State Transitions) ที่ยุ่งเหยิงซับซ้อน
ผู้เขียนย้ำเตือนว่า "ความซับซ้อนของสถานะคือแหล่งกบดานของแฮกเกอร์" เมื่อ Entity มีสถานะยุ่งเหยิง นักพัฒนามักหลุดเขียนเงื่อนไข if-else ตรวจสอบไม่ครบทุกจุด เปิดช่องให้เกิด Logic Flaws เช่น สามารถสั่งจัดส่งสินค้าออกไปได้ทั้งที่คำสั่งซื้อยังอยู่ในสถานะไม่ได้ชำระเงิน การลดความซับซ้อนของสถานะผ่านรูปแบบการออกแบบ 4 ชนิด (Partially Immutable Entities, Entity State Objects, Entity Snapshots, และ Entity Relay) จึงเป็นการตัดสภาวะสถานะที่เป็นไปไม่ได้ออกไป ส่งผลให้ช่องโหว่ความปลอดภัยถูกกำจัดออกไปโดยปริยาย!
Day 1: CIA-T / Day 2: The Anti-Hamlet / Day 3: DDD Core / Day 4: Immutability / Day 5: Domain Primitives / Day 6: Ensuring Integrity of State
Day 7 จาก 14 วัน — วันสุดท้ายของ Part 2 Fundamentals สรุปปิดฉากรากฐานสถาปัตยกรรมระดับแกนนำ!
เรียนรู้ 4 อาวุธหลักสยบความซับซ้อนของสถานะ (Partially Immutable, State Objects, Snapshots, Entity Relay) เพื่อให้ Entity ปราศจากช่องโหว่
เรื่องราวและมโนทัศน์เชิงลึก (4 State Management Patterns & Scenarios)
Single-threaded vs. Multithreaded Environments
การประมวลผลถูกจำกัดให้มีเพียง Thread เดียวเข้าถึง Entity ตัวนั้น ณ เวลาหนึ่ง (เช่น Actor Model หรือการโหลด Entity แยกอินสแตนซ์ต่อหนึ่ง Client Request) ทำให้จัดการความปลอดภัยของสถานะได้ง่ายผ่าน mutable objects
หลาย Threads เข้าถึง Entity อินสแตนซ์เดียวกันได้พร้อมกัน (เช่น การเก็บ Entity ไว้ใน Shared Cache เพื่อเน้น Performance) การใช้ synchronized ก่อให้เกิด Thread Contention และเสี่ยงต่อ Deadlock จนระบบล่ม (Loss of Availability)
📖 กฎสถานะและความผิดพลาดดั้งเดิม
พิจารณาสถานภาพการแต่งงานของบุคคล มี 2 สถานะหลัก คือ Unmarried (โสด) และ Married (แต่งงานแล้ว) คนโสดเดตได้และแต่งงานได้ เมื่อแต่งงานแล้วห้ามเดตและห้ามแต่งซ้ำ แต่หย่าเพื่อกลับมาโสดได้
if (!isMarried()) ไปซ่อนไว้ลึกๆ ตาม Service หรือในคลาส Person เมื่อระบบเติบโต มีกิจกรรมใหม่เพิ่มขึ้นมา เช่น party() นักพัฒนาคนอื่นจะ "ลืมเขียน if เช็กสถานะ" ทำให้คนแต่งงานแล้วแอบเดตได้! หากเปรียบกับร้านค้าออนไลน์ การลืม if เช็กสถานะชำระเงินในเมธอดจัดส่ง จะทำให้ของถูกส่งออกไปฟรีๆ โดยไม่จ่ายเงิน!
🛡️ ทางรอดด้วย Entity State Object
แยกตรรกะการควบคุมสถานะออกมาเป็นคลาสช่วยเฉพาะ เช่น MaritalStatus หรือ OrderState:
public class MaritalStatus { private boolean married = false; public void date() { validState(!married, "Not appropriate to date when married"); } public void marry() { validState(!married, "Already married"); this.married = true; } public void divorce() { validState(married, "Cannot divorce if not married"); this.married = false; } }
🖼️ อุปมาอุปไมย: รูปถ่ายบน Instagram
จินตนาการถึงเพื่อนสนิทที่ย้ายไปอยู่ต่างประเทศ คุณไม่ได้เจอตัวจริงของเขา (ตัวจริงเปรียบเสมือน Entity ที่แท้จริงใน Database) คุณติดตามชีวิตของเขาผ่าน "รูปถ่ายบน Instagram" รูปถ่ายแต่ละใบคือ Snapshot ณ วินาทีนั้น เป็นวัตถุเปลี่ยนค่าไม่ได้ (Immutable) คุณสามารถก๊อปปี้รูปแจกจ่ายให้คนดูพร้อมกันพันคนได้โดยที่เพื่อนของคุณไม่ต้องหยุดนิ่งหรือเดือดร้อน
⚡ สถาปัตยกรรม Entity Snapshots สำหรับระบบโหลดสูง
OrderSnapshot ซึ่งเป็น Immutable Value Object ให้ Threads อ่านพร้อมกันโดยปราศจาก Lock ("No locking, no blocking") ทนทานต่อ DoS
OrderService (Domain Service) เป็นผู้ยิงคำสั่งอัปเดตลง Database โดยตรง
🧶 ฝันร้ายของไหมพรม 15 สถานะ
เมื่อระบบเปิดใช้หลายปี คำสั่งซื้อเติบโตจาก 2 สถานะ กลายเป็น 15 สถานะ: CONFIGURING ➔ PAID ➔ SHIPPED ➔ DELIVERED ➔ REJECTED ➔ UNDER_RETURN ➔ RETURNED ➔ REPAID ➔ MISPLACED ➔ LOST... การยัดทั้งหมดไว้ใน Order ตัวเดียวทำให้เกิดเส้นทางประหลาดที่เปิดช่องโหว่ความปลอดภัย
🏃♂️ การแก้ปัญหาด้วย Entity Relay (วิ่งส่งไม้คบเพลิง)
แบ่งช่วงชีวิตออกเป็นเฟสๆ และสร้าง Entity ประจำแต่ละเฟสมารับไม้ต่อ:
PreliminaryOrder (เลือกซื้อและชำระเงิน - 4-5 สถานะ)DefinitiveOrder (จัดส่งพัสดุ - 4-5 สถานะ)RejectedOrder (ส่งคืนและคืนเงิน - 3-4 สถานะ)
ล็อคฟิลด์ที่ไม่ควรเปลี่ยน (เช่น Identity, CreationDate) เป็น private final เพื่อลดส่วนที่เคลื่อนไหวได้ (Moving Parts) และให้คอมไพเลอร์ช่วยตรวจตั้งแต่ Compile-time
แยกกฎการเปลี่ยนสถานะออกมาเป็น Helper Class เฉพาะ (เช่น OrderState) แล้วให้ Entity ทำการ Delegate ตรวจสอบก่อนเสมอ รวมศูนย์การตรวจสอบความปลอดภัยไว้ที่เดียว
ส่งออกเฉพาะภาพถ่ายสถานะที่เป็น Immutable Value Object ให้ฝั่งอ่านเรียกใช้ตามแนวคิด CQRS "No locking, no blocking" ปลอดภัยจากการแอบแก้ไขสถานะ
หั่นช่วงชีวิตของ Entity ที่มี 10+ สถานะออกเป็นเฟสๆ วิ่งส่งไม้ต่อกันไปข้างหน้าโดยไม่มีลูปย้อนกลับ ลดขนาด State Graph ให้เข้าใจง่ายและปราศจาก Logic Flaws
แบบฝึกหัดปฏิบัติตามสถานการณ์จริง (Hands-on Refactoring)
// โค้ดเดิม: รวมทุกสถานะไว้ใน Order เดียว เช็ก if สับสน public class Order { private String status; // "CONFIGURING", "PAID", "SHIPPED"... private List<OrderItem> items; public void ship() { if (this.status.equals("CONFIGURING")) { throw new IllegalStateException("Under configuration!"); } // ช่องโหว่ร้ายแรง: ลืมเช็ก if (!this.status.equals("PAID")) !! this.status = "SHIPPED"; // แฮกเกอร์สั่งส่งของได้ทั้งที่ยังไม่ได้จ่ายเงิน! } }
if-else พันกันเป็นไหมพรม เมธอด ship() ลืมตรวจสถานะ PAID ทำให้ส่งสินค้าออกไปได้โดยไม่จ่ายเงิน!
// 1. Entity เฟสแรก: PreliminaryOrder (เลือกซื้อและชำระเงิน) public final class PreliminaryOrder { private final OrderId id; private final List<OrderItem> items; private boolean paid = false; public PreliminaryOrder(OrderId id, List<OrderItem> items) { this.id = notNull(id); this.items = Collections.unmodifiableList(new ArrayList<>(items)); } // ชำระเงินเสร็จ ให้ "สร้างและส่งไม้ต่อ" ให้ DefinitiveOrder public DefinitiveOrder pay(PaymentReceipt payment) { notNull(payment, "Payment receipt required"); validState(!paid, "Order is already paid"); this.paid = true; return new DefinitiveOrder(this.id, this.items, payment); } } // 2. Entity เฟสสอง: DefinitiveOrder (จัดส่งพัสดุ - การันตี paid แล้ว 100%) public final class DefinitiveOrder { private final OrderId id; private final List<OrderItem> items; private final PaymentReceipt payment; private ShippingStatus status = ShippingStatus.PREPARING; DefinitiveOrder(OrderId id, List<OrderItem> items, PaymentReceipt payment) { this.id = notNull(id); this.items = notNull(items); this.payment = notNull(payment); // มีใบเสร็จการันตีตั้งแต่กำเนิด! } public void ship() { validState(this.status == ShippingStatus.PREPARING, "Cannot ship"); this.status = ShippingStatus.SHIPPED; // ปลอดภัย 100% ไม่มีทางส่งของโดยไม่จ่ายเงิน! } }
PreliminaryOrder และ DefinitiveOrder ออกจากกันอย่างเด็ดขาด DefinitiveOrder ไม่ต้องเขียน if เช็กว่าจ่ายเงินหรือยัง เพราะมันถือกำเนิดขึ้นได้ก็ต่อเมื่อจ่ายเงินแล้วเท่านั้น (Consistent on Creation) ปิดช่องโหว่การข้ามสถานะอย่างสมบูรณ์แบบ!
ชุดควิซประเมินความรู้ประจำวัน
ตอบปุ๊บ รู้ผลเฉลยทันที พร้อมคำอธิบายและหลอดพลังงาน Energy Charge
ทดสอบความรู้ Day 7 สำเร็จแล้ว!
สรุปการเชื่อมโยงกลับสู่ Chapter 7 — Reducing complexity of state
การศึกษาเทคนิคทั้ง 4 ในวันนี้ (Partially Immutable Entities, Entity State Objects, Entity Snapshots, และ Entity Relay) ล้วนตอบคำถามหัวข้อบท "Reducing complexity of state" อย่างสมบูรณ์แบบว่า ความซับซ้อนของสถานะคือศัตรูตัวร้ายของความปลอดภัย การลดความซับซ้อนโดยการแช่แข็งฟิลด์ที่ไม่เปลี่ยน, แยกคลาสช่วยควบคุมสถานะ, ถ่าย Snapshot สำหรับอ่านขนานกัน, และแบ่งเฟสการวิ่งส่งไม้ต่อ จะช่วยย่อยสลายความยุ่งเหยิง ตัดเส้นทางที่ผิดกฎทางธุรกิจออกไป และสร้างซอฟต์แวร์ที่ปลอดภัย แข็งแกร่ง และทรงประสิทธิภาพอย่างยั่งยืนครับ!