State
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”State ยอมให้ object เปลี่ยนแปลงพฤติกรรมเมื่อสถานะภายในของตัวเองเปลี่ยน แต่ละสถานะกลายเป็น object ของตัวเอง และ context มอบหมายงานให้สถานะใดก็ตามที่ถืออยู่ในขณะนั้น เมื่อมองจากภายนอก context ดูเหมือนเปลี่ยน class ในขณะรัน
ลองพิจารณาออเดอร์ที่เคลื่อนผ่าน draft, paid, shipped และ delivered แต่ละแอ็กชัน ได้แก่ pay, ship, deliver, cancel ถูกอนุญาตในบางสถานะและถูกห้ามในบางสถานะ การ implement แบบไร้เดียงสาคือ string สถานะกับพงรกของเงื่อนไขในทุก method คือ “ถ้าสถานะเป็น draft ทำสิ่งนี้ ไม่งั้นถ้า paid ทำสิ่งนั้น ไม่งั้นโยน error” กฎสำหรับแต่ละสถานะถูกป้ายเลอะอยู่ทั่วทุก method และการเพิ่มสถานะหมายถึงการต้องกลับไปดูทุก method อีกครั้ง
ความผันแปรในที่นี้คือ พฤติกรรมต่อสถานะ และการเปลี่ยนผ่านระหว่างสถานะ State รวบรวมพฤติกรรมทั้งหมดสำหรับหนึ่งสถานะไว้ใน object เดียว context ถือ reference ไปยังสถานะปัจจุบันและส่งต่อการเรียกไปยังสถานะนั้น แต่ละสถานะรู้ว่าตัวเองอนุญาตอะไรและจะเคลื่อนไปยังสถานะใดต่อไป การเพิ่มสถานะคือการเพิ่ม class และกฎการเปลี่ยนผ่านก็อยู่ติดกับพฤติกรรมที่ควบคุม
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Order {
-state: OrderState
+pay()
+ship()
+status() string
}
class OrderState {
<<interface>>
+pay(order)
+ship(order)
+name() string
}
class DraftState {
+pay(order)
+ship(order)
}
class PaidState {
+pay(order)
+ship(order)
}
Order o--> OrderState
OrderState <|.. DraftState
OrderState <|.. PaidState - State — interface ที่ประกาศแอ็กชันที่ผันแปรตามสถานะ
- Concrete State — object สถานะหนึ่งตัว implement แอ็กชันที่อนุญาตและกระตุ้นการเปลี่ยนผ่านไปยังสถานะอื่น
- Context — ถือ reference ไปยังสถานะปัจจุบันและมอบหมายทุกการเรียกที่ขึ้นกับสถานะให้สถานะนั้น พร้อมเปิดเผยวิธีให้สถานะสลับ context ไปยังสถานะใหม่
- Client — ขับเคลื่อน context โดยการเรียกแอ็กชันของตัวเอง ไม่จัดการสถานะโดยตรง
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”state machine ของออเดอร์ที่แต่ละสถานะตัดสินว่า pay และ ship ทำอะไรและสถานะใดมาต่อไป เปรียบเทียบกับ Strategy ที่ client เลือกอัลกอริทึม ส่วนที่นี่สถานะเลือกตัวต่อจากตัวเอง
interface OrderState { name(): string; pay(order: Order): void; ship(order: Order): void;}
class Order { private state: OrderState = new DraftState(); setState(s: OrderState): void { this.state = s; } status(): string { return this.state.name(); } pay(): void { this.state.pay(this); } ship(): void { this.state.ship(this); }}
class DraftState implements OrderState { name(): string { return 'draft'; } pay(order: Order): void { order.setState(new PaidState()); } ship(_order: Order): void { throw new Error('cannot ship an unpaid order'); }}
class PaidState implements OrderState { name(): string { return 'paid'; } pay(_order: Order): void { throw new Error('already paid'); } ship(order: Order): void { order.setState(new ShippedState()); }}
class ShippedState implements OrderState { name(): string { return 'shipped'; } pay(_order: Order): void { throw new Error('already paid'); } ship(_order: Order): void { throw new Error('already shipped'); }}
const order = new Order();order.pay();order.ship();console.log(order.status()); // "shipped"from __future__ import annotationsfrom typing import Protocol
class OrderState(Protocol): def name(self) -> str: ... def pay(self, order: Order) -> None: ... def ship(self, order: Order) -> None: ...
class Order: def __init__(self) -> None: self._state: OrderState = DraftState()
def set_state(self, state: OrderState) -> None: self._state = state
def status(self) -> str: return self._state.name()
def pay(self) -> None: self._state.pay(self)
def ship(self) -> None: self._state.ship(self)
class DraftState: def name(self) -> str: return "draft"
def pay(self, order: Order) -> None: order.set_state(PaidState())
def ship(self, order: Order) -> None: raise ValueError("cannot ship an unpaid order")
class PaidState: def name(self) -> str: return "paid"
def pay(self, order: Order) -> None: raise ValueError("already paid")
def ship(self, order: Order) -> None: order.set_state(ShippedState())
class ShippedState: def name(self) -> str: return "shipped"
def pay(self, order: Order) -> None: raise ValueError("already paid")
def ship(self, order: Order) -> None: raise ValueError("already shipped")
order = Order()order.pay()order.ship()print(order.status()) # "shipped"package main
import ( "errors" "fmt")
type Order struct{ state OrderState }
// OrderState decides what is allowed and which state comes next.type OrderState interface { Name() string Pay(o *Order) error Ship(o *Order) error}
func (o *Order) setState(s OrderState) { o.state = s }func (o *Order) Status() string { return o.state.Name() }func (o *Order) Pay() error { return o.state.Pay(o) }func (o *Order) Ship() error { return o.state.Ship(o) }
type DraftState struct{}
func (DraftState) Name() string { return "draft" }func (DraftState) Pay(o *Order) error { o.setState(PaidState{}) return nil}func (DraftState) Ship(*Order) error { return errors.New("cannot ship an unpaid order") }
type PaidState struct{}
func (PaidState) Name() string { return "paid" }func (PaidState) Pay(*Order) error { return errors.New("already paid") }func (PaidState) Ship(o *Order) error { o.setState(ShippedState{}) return nil}
type ShippedState struct{}
func (ShippedState) Name() string { return "shipped" }func (ShippedState) Pay(*Order) error { return errors.New("already paid") }func (ShippedState) Ship(*Order) error { return errors.New("already shipped") }
func main() { order := &Order{state: DraftState{}} _ = order.Pay() _ = order.Ship() fmt.Println(order.Status()) // "shipped"}enum Transition { Stay, To(Box<dyn OrderState>), Error(&'static str),}
trait OrderState { fn name(&self) -> &'static str; fn pay(&self) -> Transition; fn ship(&self) -> Transition;}
struct Draft;impl OrderState for Draft { fn name(&self) -> &'static str { "draft" } fn pay(&self) -> Transition { Transition::To(Box::new(Paid)) } fn ship(&self) -> Transition { Transition::Error("cannot ship an unpaid order") }}
struct Paid;impl OrderState for Paid { fn name(&self) -> &'static str { "paid" } fn pay(&self) -> Transition { Transition::Error("already paid") } fn ship(&self) -> Transition { Transition::To(Box::new(Shipped)) }}
struct Shipped;impl OrderState for Shipped { fn name(&self) -> &'static str { "shipped" } fn pay(&self) -> Transition { Transition::Error("already paid") } fn ship(&self) -> Transition { Transition::Error("already shipped") }}
struct Order { state: Box<dyn OrderState>,}impl Order { fn apply(&mut self, t: Transition) { if let Transition::To(next) = t { self.state = next; } } fn pay(&mut self) { let t = self.state.pay(); self.apply(t); } fn ship(&mut self) { let t = self.state.ship(); self.apply(t); } fn status(&self) -> &'static str { self.state.name() }}
fn main() { let mut order = Order { state: Box::new(Draft) }; order.pay(); order.ship(); println!("{}", order.status()); // "shipped"}ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”- ข้อดี: แทนที่เงื่อนไขต่อสถานะที่พันกันยุ่งด้วย object ที่เกาะกลุ่มกันหนึ่งตัวต่อสถานะ
- ข้อดี: การเปลี่ยนผ่านเป็นไปอย่างชัดเจนและอยู่ข้างพฤติกรรมที่กระตุ้น ซึ่งทำให้ state machine อ่านง่าย
- ข้อดี: การเพิ่มสถานะหมายถึงการเพิ่ม class โดยไม่แตะต้องสถานะที่มีอยู่
- ข้อเสีย: มี class มากกว่าแฟล็กสถานะตัวเดียว ซึ่งเกินจำเป็นสำหรับสองหรือสามสถานะเล็กน้อย
- ข้อเสีย: ตรรกะการเปลี่ยนผ่านกระจายอยู่ทั่วสถานะต่าง ๆ แผนที่เต็มของ machine จึงไม่ได้อยู่ที่เดียว
pattern ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “pattern ที่เกี่ยวข้อง”- Strategy ใช้ไดอะแกรม class เดียวกัน แต่ intent แยกทางกัน strategy ถูกเลือกโดย client เพื่อทำให้อัลกอริทึมผันแปร ส่วน object สถานะเลือกตัวต่อจากตัวเองเมื่อเงื่อนไขของ object เปลี่ยน object Strategy มักไม่รับรู้ถึงกันและกัน object สถานะรู้จักสถานะที่เปลี่ยนผ่านไป
- Singleton บางครั้งถูกใช้กับ object สถานะที่ไร้สถานะ เพื่อให้แชร์ใช้ได้แทนที่จะสร้างใหม่ทุกครั้งที่เปลี่ยนผ่าน
| State | Strategy | Chain of Responsibility | |
|---|---|---|---|
| ใครเปลี่ยน behavior | object เองเมื่อ state เปลี่ยน | client inject จากนอก | ส่งต่อตาม chain จนมีคนรับ |
| รู้จักกัน | state object รู้จัก transition | ไม่รู้จักกัน | handler รู้จัก next |
| จำนวน behavior | หนึ่งตัวที่ active | หนึ่งตัวที่เลือกไว้ | หลายตัว (ใครจัดการก็ได้) |
| เมื่อใช้ | FSM, order status, connection | algorithm ที่สลับกันได้ | auth, validation, middleware |