State
จำลองการเปลี่ยนสถานะด้วย object แยกต่อแต่ละสถานะ (FSM)
หลาย object มีพฤติกรรมที่เปลี่ยนไปตาม “สถานะภายใน” ของมันเอง เช่น คำสั่งซื้อ (Order) ที่ทำงานต่างกันตอนเป็น Draft, Pending, Approved หรือ Rejected วิธีเขียนที่ไร้เดียงสาที่สุดคือเก็บ field แบบ enum ไว้ใน class เดียว แล้วใช้ if/switch เช็คค่านั้นในแทบทุก method — ยิ่งจำนวนสถานะและ transition เพิ่มขึ้น conditional ก็ยิ่งพันกันจนแก้ยาก และเสี่ยงลืมอัปเดตบางจุดเวลาสถานะใหม่ถูกเพิ่มเข้ามา
State pattern แก้ปัญหานี้ด้วยการดึงพฤติกรรมเฉพาะของแต่ละสถานะออกมาเป็น class ของตัวเอง แล้วให้ object หลัก (เรียกว่า Context) ถือ reference ไปยัง state object ปัจจุบัน และ ส่งต่อ (delegate) ทุกการเรียกที่ขึ้นกับสถานะไปให้ state object นั้นจัดการแทน เวลาสถานะเปลี่ยน Context แค่สลับ reference ไปชี้ state object ใหม่ — จากมุมมองภายนอกมันดูเหมือน object เปลี่ยน class ของตัวเองไปเลย
มองอีกมุม1 pattern นี้คือการนำ Finite State Machine (FSM) มาเขียนด้วย OOP: แต่ละสถานะ (node) เป็น subtype หนึ่งของ state ฐาน ที่นิยาม method สำหรับทุก transition (edge) ที่เป็นไปได้ในระบบ subclass แต่ละตัว implement เฉพาะ transition ที่ตัวเองอนุญาตให้เกิดขึ้น ส่วนการดำเนินการที่ไม่ถูกต้องในสถานะนั้นจะโยน exception หรือไม่มีผลใด ๆ — ใช้ polymorphism แทน conditional และเชื่อมโยงโดยตรงกับหลักการ Make Illegal States Unrepresentable
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Context {
-IOrderState state
+Submit()
+Approve()
+Reject()
+TransitionTo(IOrderState s)
}
class IOrderState {
+Submit(Context c)
+Approve(Context c)
+Reject(Context c)
}
class DraftState {
+Submit(Context c)
}
class PendingState {
+Approve(Context c)
+Reject(Context c)
}
class ApprovedState
class RejectedState
Context o-- IOrderState
IOrderState <|.. DraftState
IOrderState <|.. PendingState
IOrderState <|.. ApprovedState
IOrderState <|.. RejectedState
- Context — class ที่ client เห็นและเรียกใช้งาน มันถือ reference ไปยัง state object ปัจจุบัน แต่ ไม่รู้รายละเอียด ว่าแต่ละสถานะทำงานอย่างไร มันแค่ delegate ให้ state object จัดการ
- State (interface/abstract class) — นิยาม contract ของทุก operation ที่ขึ้นกับสถานะ
- Concrete State — implement เฉพาะ transition ที่ตัวเองอนุญาต และมักเป็นผู้สั่งให้ Context เปลี่ยนไปยัง state object ถัดไป (state จึงมัก “รู้จัก” กันเองว่าจะไปสถานะไหนต่อ)
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”- Client เรียก method บน Context เช่น
context.Approve() - Context ไม่ตัดสินใจเอง แต่ delegate การเรียกไปยัง state object ปัจจุบัน (
state.Approve(this)) - Concrete state ตรวจสอบว่า transition นี้ถูกกฎหรือไม่ ถ้าถูกกฎ มันจะสร้าง/เลือก state object ถัดไป แล้วสั่งให้ Context เปลี่ยน reference ไปชี้ state ใหม่ (
context.TransitionTo(new ApprovedState())) - ถ้า transition ไม่ถูกกฎในสถานะปัจจุบัน (เช่นเรียก
Approve()ตอนยังเป็น Draft) จะโยน exception หรือถูก no-op ไปเลย — ไม่ต้องมีif (state == "Pending")กระจายอยู่ทั่ว code
sequenceDiagram
participant Client
participant Context
participant Pending as PendingState
participant Approved as ApprovedState
Client->>Context: Approve()
Context->>Pending: Approve(context)
Pending->>Context: TransitionTo(ApprovedState)
Context-->>Client: OK
จุดที่มักเป็นประเด็นในการออกแบบคือ ใครเป็นคนสร้าง/ถือ state object ถัดไป — จะให้ concrete state แต่ละตัวรู้จักกันเอง (state ต่อ state ตัดสินใจ) หรือให้ Context เป็นศูนย์กลางตัดสินใจ transition ทั้งหมด (มัก implement ด้วย dictionary ของ (currentState, event) -> nextState) ทั้งสองแบบถูกต้องตาม pattern แต่ trade-off เรื่อง coupling ต่างกัน
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”// สถานะฐาน — นิยาม operation ทั้งหมดที่ Context รองรับ// ค่า default คือ "ไม่อนุญาต" เพื่อบังคับให้แต่ละสถานะเปิดเฉพาะ transition ที่ถูกต้องpublic abstract class OrderState{ public abstract string Name { get; }
public virtual void Submit(OrderContext order) => throw new InvalidOperationException($"ส่ง (Submit) จากสถานะ {Name} ไม่ได้");
public virtual void Approve(OrderContext order) => throw new InvalidOperationException($"อนุมัติ (Approve) จากสถานะ {Name} ไม่ได้");
public virtual void Reject(OrderContext order) => throw new InvalidOperationException($"ปฏิเสธ (Reject) จากสถานะ {Name} ไม่ได้");}
public class DraftState : OrderState{ public override string Name => "Draft";
public override void Submit(OrderContext order) => order.TransitionTo(new PendingState());}
public class PendingState : OrderState{ public override string Name => "Pending";
public override void Approve(OrderContext order) => order.TransitionTo(new ApprovedState());
public override void Reject(OrderContext order) => order.TransitionTo(new RejectedState());}
public class ApprovedState : OrderState{ public override string Name => "Approved"; // ไม่ override method ใดเพิ่ม — ทุก transition จาก Approved ถือเป็นสถานะสุดท้าย}
public class RejectedState : OrderState{ public override string Name => "Rejected";}
// Context — ถือสถานะปัจจุบัน และ delegate ทุกการเรียกให้มันจัดการpublic class OrderContext{ public OrderState State { get; private set; } = new DraftState();
public void Submit() => State.Submit(this); public void Approve() => State.Approve(this); public void Reject() => State.Reject(this);
// เปิดให้เฉพาะ state object เท่านั้นเป็นผู้สั่งเปลี่ยนสถานะ internal void TransitionTo(OrderState next) { Console.WriteLine($"{State.Name} -> {next.Name}"); State = next; }}
// การใช้งานvar order = new OrderContext();order.Submit(); // Draft -> Pendingorder.Approve(); // Pending -> Approvedorder.Reject(); // โยน InvalidOperationException — Approved ปฏิเสธไม่ได้แล้วเมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- object มีพฤติกรรมที่ต่างกันชัดเจนตามสถานะ และจำนวนสถานะ/transition มีพอสมควร (ตั้งแต่ 3-4 สถานะขึ้นไป)
- code ปัจจุบันเต็มไปด้วย
if/switchที่เช็ค field แบบ enum ซ้ำ ๆ ในหลาย method (ตรงกับ smell Conditional Complexity) - ต้องการเพิ่มสถานะใหม่บ่อย ๆ โดยไม่อยากแก้ code เดิม (สอดคล้องกับ Open-Closed Principle)
- ต้องการบังคับ transition ที่ผิดกฎให้ fail ทันทีที่ compile-time หรือ runtime แรกสุด แทนที่จะปล่อยให้ object อยู่ในสถานะที่ไม่ควรมีอยู่จริง
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- สถานะมีแค่ 2 ค่า (on/off) หรือ transition ไม่ซับซ้อน —
boolหรือenumธรรมดาก็เพียงพอ และอ่านง่ายกว่า - ทีมต้องการเห็นตาราง transition ทั้งหมดในที่เดียวเพื่อ audit/debug ง่าย — การกระจาย logic ไปหลาย class อาจทำให้ตามรอย transition ยากขึ้น (กรณีนี้ table-driven state machine หรือ library อย่าง state machine ภายนอกอาจเหมาะกว่า)
- สถานะแทบไม่เพิ่มและไม่เปลี่ยนพฤติกรรมอีกแล้ว — ต้นทุนสร้าง class เพิ่มไม่คุ้มกับความยืดหยุ่นที่ได้
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | กำจัด conditional ก้อนใหญ่ออกจาก Context ทำให้แต่ละ method สั้นและอ่านง่ายขึ้น |
| ข้อดี | เพิ่มสถานะใหม่ได้โดยไม่แก้ code เดิม (Open-Closed) — เขียน class ใหม่แล้ว implement interface |
| ข้อดี | ทำให้ transition ที่ไม่ถูกต้องเป็น “impossible by construction” หรือ fail ชัดเจนทันที แทนที่จะปล่อยผ่านเงียบ ๆ |
| ข้อเสีย | จำนวน class เพิ่มขึ้นตามจำนวนสถานะ — สำหรับ FSM เล็ก ๆ อาจดู over-engineered |
| ข้อเสีย | transition logic กระจายอยู่หลาย class ทำให้มองภาพรวม FSM ทั้งระบบได้ยากกว่าตาราง transition แบบรวมศูนย์ |
| ข้อเสีย | ถ้า concrete state ต้อง “รู้จัก” กันเองเพื่อสร้าง state ถัดไป จะเกิด coupling ระหว่าง concrete state ด้วยกัน |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Make Illegal States Unrepresentable — เป้าหมายปลายทางที่ State pattern ช่วยให้เข้าใกล้ได้
- Strategy — โครงสร้างคล้ายกันมาก (Context ถือ interface แล้ว delegate) แต่ Strategy ไม่มีแนวคิดเรื่อง transition ระหว่าง strategy ด้วยกันเอง ส่วน State ออกแบบมาให้ concrete state เปลี่ยนสถานะของ Context ได้เอง
- Open-Closed Principle — เหตุผลหลักที่ทำให้เพิ่มสถานะใหม่ได้โดยไม่แก้ code เดิม
- Conditional Complexity — code smell ที่ State pattern มักถูกใช้แก้ไข
- Observer — มักใช้ร่วมกันเพื่อแจ้งผู้สนใจเมื่อ Context เปลี่ยนสถานะ
- Memento — ใช้เก็บ snapshot ของสถานะเพื่อ undo/redo ซึ่งเป็นแนวคิดที่ต่างจาก State แต่มักถูกพูดถึงคู่กันเพราะทำงานกับ “สถานะ” เหมือนกัน