Replace Type Code with State/Strategy
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”นำ type code ที่ทั้ง ขับเคลื่อน behavior ที่แตกต่าง และ เปลี่ยนไปตลอดอายุของ object — status ของ order, mode ของ connection — มาแทนที่ด้วย object State (หรือ Strategy) ที่ host ถือไว้และสลับได้ host มอบหมาย behavior ที่แตกต่างให้ object นั้น การเปลี่ยนสถานะก็แค่กำหนด state ตัวใหม่ เพราะ class ของ host ไม่เคยเปลี่ยน วิธีนี้จึงใช้ได้พอดีตรงที่ Replace Type Code with Subclasses ทำไม่ได้
อาการของปัญหา
หัวข้อที่มีชื่อว่า “อาการของปัญหา”smell คือ switch ที่กระจัดกระจายบน type code แบบเดียวกับใน refactoring ของ subclass แต่มี property เพิ่มมาหนึ่งอย่าง: code เปลี่ยนได้ตอน runtime order เลื่อน pending → shipped → delivered; document ไป draft → published → archived คุณไม่สามารถจำลอง identity ที่เปลี่ยนได้ด้วยการเปลี่ยน class ของ object — ภาษาส่วนใหญ่ไม่ยอมให้ object กลายเป็น class อื่นกลางอายุ ดังนั้นการแตกกิ่งจึงทวีคูณไปทั่ว method และทุกการเปลี่ยนสถานะคือการกำหนดค่า field ดิบโดยไม่มีหลักประกันว่าค่าใหม่นั้นถูกต้องด้วยซ้ำ
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”Order ที่ string status ถูกตรวจด้วย switch ในสอง method หลังจากนั้น order ถือ OrderState ที่มอบหมายให้และสลับเมื่อมีการเปลี่ยนสถานะ
// Beforeclass Order { constructor(public status: string) {} label(): string { switch (this.status) { case 'pending': return 'Awaiting payment'; case 'shipped': return 'On the way'; case 'delivered': return 'Complete'; default: throw new Error(`unknown ${this.status}`); } } canCancel(): boolean { return this.status === 'pending'; }}
// Afterinterface OrderState { label(): string; canCancel(): boolean;}const Pending: OrderState = { label: () => 'Awaiting payment', canCancel: () => true };const Shipped: OrderState = { label: () => 'On the way', canCancel: () => false };const Delivered: OrderState = { label: () => 'Complete', canCancel: () => false };
class Order { constructor(private state: OrderState = Pending) {} label(): string { return this.state.label(); } canCancel(): boolean { return this.state.canCancel(); } ship(): void { this.state = Shipped; } // a transition is a swap}# Beforeclass Order: def __init__(self, status): self.status = status def label(self): if self.status == "pending": return "Awaiting payment" elif self.status == "shipped": return "On the way" elif self.status == "delivered": return "Complete" raise ValueError(self.status) def can_cancel(self): return self.status == "pending"
# Afterfrom typing import Protocol
class OrderState(Protocol): def label(self) -> str: ... def can_cancel(self) -> bool: ...
class Pending: def label(self): return "Awaiting payment" def can_cancel(self): return True
class Shipped: def label(self): return "On the way" def can_cancel(self): return False
class Delivered: def label(self): return "Complete" def can_cancel(self): return False
class Order: def __init__(self, state=None): self.state = state or Pending() def label(self): return self.state.label() def can_cancel(self): return self.state.can_cancel() def ship(self): self.state = Shipped() # a transition is a swap// Beforetype Order struct{ Status string }func (o Order) Label() string { switch o.Status { case "pending": return "Awaiting payment" case "shipped": return "On the way" case "delivered": return "Complete" default: panic("unknown " + o.Status) }}func (o Order) CanCancel() bool { return o.Status == "pending" }
// Aftertype OrderState interface { Label() string CanCancel() bool}type pending struct{}func (pending) Label() string { return "Awaiting payment" }func (pending) CanCancel() bool { return true }
type shipped struct{}func (shipped) Label() string { return "On the way" }func (shipped) CanCancel() bool { return false }
type Order struct{ state OrderState }func NewOrder() *Order { return &Order{state: pending{}} }func (o *Order) Label() string { return o.state.Label() }func (o *Order) CanCancel() bool { return o.state.CanCancel() }func (o *Order) Ship() { o.state = shipped{} } // swap// Beforestruct Order { status: String }impl Order { fn label(&self) -> &str { match self.status.as_str() { "pending" => "Awaiting payment", "shipped" => "On the way", "delivered" => "Complete", other => panic!("unknown {other}"), } } fn can_cancel(&self) -> bool { self.status == "pending" }}
// Aftertrait OrderState { fn label(&self) -> &str; fn can_cancel(&self) -> bool;}struct Pending;impl OrderState for Pending { fn label(&self) -> &str { "Awaiting payment" } fn can_cancel(&self) -> bool { true }}struct Shipped;impl OrderState for Shipped { fn label(&self) -> &str { "On the way" } fn can_cancel(&self) -> bool { false }}
struct Order { state: Box<dyn OrderState> }impl Order { fn new() -> Self { Order { state: Box::new(Pending) } } fn label(&self) -> &str { self.state.label() } fn can_cancel(&self) -> bool { self.state.can_cancel() } fn ship(&mut self) { self.state = Box::new(Shipped); } // swap}flowchart TD
subgraph Before["Before — a type code with switching"]
A["Order<br/>status: 'pending' | 'shipped' | 'delivered'"]
A --> B{"switch status"}
B --> C["pending behaviour"]
B --> D["shipped behaviour"]
B --> E["delivered behaviour"]
end
subgraph After["After — a swappable state object"]
F["Order<br/>holds a State"]
F --> G["OrderState (interface)<br/>label() / canCancel()"]
G --> H["Pending"]
G --> I["Shipped"]
G --> J["Delivered"]
end
Before -.->|"Replace Type Code with State/Strategy"| After กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- Encapsulate field ของ type code ไว้หลัง accessor เพื่อให้ caller พึ่งพา getter/setter แทนค่าดิบ
- นำ interface ของ state เข้ามา ประกาศ operation ที่แตกต่างไปตาม code
- สร้าง class state หนึ่งตัวต่อหนึ่งค่าของ type code ย้ายแต่ละกิ่งของ method ที่แตกต่างเข้าไปยัง state ที่สอดคล้องกัน
- ให้ host มี field ที่ถือ state ปัจจุบัน และให้ method มอบหมายไปยัง state นั้น รัน test หลังต่อสาย method แต่ละตัว
- แทนที่การกำหนดค่า type code แต่ละครั้งด้วยการสลับ object state — ในอุดมคติผ่าน method อย่าง
ship()ที่ตั้งชื่อให้การเปลี่ยนสถานะและคอยกั้นว่าการเปลี่ยนแบบไหนถูกกฎ - เมื่อไม่เหลืออะไรอ่านค่า type code ดิบแล้ว ก็ลบทิ้งได้ ตอนนี้ host เปลี่ยน behavior ด้วยการเลือกว่าจะถือ state ตัวไหนล้วน ๆ
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”เลือก State/Strategy เมื่อ variant เปลี่ยนได้หลังการสร้าง เพราะ host คง class ไว้และเพียงสลับ object ที่ถือ จึงจำลอง lifecycle ที่ subclass ทำไม่ได้ State เหมาะกับชุดของเงื่อนไขที่มีชื่อจำนวนน้อยพร้อมการเปลี่ยนระหว่างกัน (สถานะของ order); Strategy เหมาะกับการฉีด algorithm ที่ใช้แทนกันได้หนึ่งตัว (นโยบายการตั้งราคา) ทั้งคู่ยังให้คุณกั้นการเปลี่ยนสถานะและกระทั่งเพิ่มข้อมูลต่อ state ที่ host ไม่ต้องแบกไว้
เปรียบเทียบกับ Replace Type Code with Subclasses: การย้ายนั้นอบ variant ลงใน type ของ object ซึ่งเหมาะที่สุดเมื่อ type ตายตัวตลอดชีวิต (บทบาทของพนักงาน) แต่เป็นไปไม่ได้เมื่อต้องเปลี่ยน ต้นทุนของ State/Strategy คือ object เพิ่มอีกหนึ่งตัวกับชั้น delegation อีกชั้น ซึ่งคุณจ่ายไปเพื่อแลกกับ lifecycle ที่สลับได้ตอน runtime และกั้นขอบเขตได้ ถ้า type ไม่เคยเปลี่ยนเลย ใช้ subclass ง่ายกว่า
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Replace Type Code with State/Strategy เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| behavior เปลี่ยนตาม type code และเปลี่ยนได้ใน runtime | type code คงที่ตลอด lifecycle ของ object |
| type code ถูกใช้ใน switch ที่ซ้ำในหลายที่ | มีแค่ 2-3 type และไม่น่าจะเพิ่ม |
| ต้องการเพิ่ม type ใหม่โดยไม่แก้ code เดิม | subclass เป็นตัวเลือกที่ง่ายกว่าและ type ไม่เปลี่ยน |
⚠️ ไม่ควร Replace Type Code with State/Strategy เมื่อ:
- type code ไม่ส่งผลต่อ behavior — เป็นแค่ label
- ต้องการ simple serialization — object ซับซ้อนกว่า enum
- มีแค่ 2 state ที่ชัดเจน — boolean flag เพียงพอ