ข้ามไปยังเนื้อหา

Replace Type Code with State/Strategy

นำ type code ที่ทั้ง ขับเคลื่อน behavior ที่แตกต่าง และ เปลี่ยนไปตลอดอายุของ objectstatus ของ 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 ที่มอบหมายให้และสลับเมื่อมีการเปลี่ยนสถานะ

// Before
class 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';
}
}
// After
interface 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
}
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
type code ที่เปลี่ยนตอน runtime กลายเป็น object State ที่สลับได้ซึ่ง host มอบหมายให้
  1. Encapsulate field ของ type code ไว้หลัง accessor เพื่อให้ caller พึ่งพา getter/setter แทนค่าดิบ
  2. นำ interface ของ state เข้ามา ประกาศ operation ที่แตกต่างไปตาม code
  3. สร้าง class state หนึ่งตัวต่อหนึ่งค่าของ type code ย้ายแต่ละกิ่งของ method ที่แตกต่างเข้าไปยัง state ที่สอดคล้องกัน
  4. ให้ host มี field ที่ถือ state ปัจจุบัน และให้ method มอบหมายไปยัง state นั้น รัน test หลังต่อสาย method แต่ละตัว
  5. แทนที่การกำหนดค่า type code แต่ละครั้งด้วยการสลับ object state — ในอุดมคติผ่าน method อย่าง ship() ที่ตั้งชื่อให้การเปลี่ยนสถานะและคอยกั้นว่าการเปลี่ยนแบบไหนถูกกฎ
  6. เมื่อไม่เหลืออะไรอ่านค่า 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 และเปลี่ยนได้ใน runtimetype 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 เพียงพอ
property เพิ่มเติมใดที่ผลักคุณไปทาง State/Strategy แทน subclass?
หลังการ refactor นี้ การเปลี่ยนสถานะระหว่าง variant ถูกแสดงออกอย่างไร?
เมื่อไรที่ Replace Type Code with Subclasses เป็นทางเลือกที่ดีกว่า?
ต้นทุนตามปกติของการเลือก State/Strategy คืออะไร?