Replace Type Code with Subclasses
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”นำ field ที่ถือ Type Code — enum หรือ string อย่าง "engineer", "manager", "sales" — ที่ขับเคลื่อน behavior เชิงเงื่อนไข แล้วแทนที่แต่ละกิ่งด้วย Subclass class ฐานที่ใช้ร่วมกันประกาศการทำงาน ส่วน Subclass แต่ละตัวจัดเตรียมเวอร์ชันของตัวเอง switch หายวับไป และการเพิ่มชนิดใหม่ก็หมายถึงการเพิ่ม class แทนที่จะแก้ไขทุกเงื่อนไข
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”กลิ่นเหม็นคือ Type Code ที่จับคู่กับ logic switching: field เดียวที่ค่าถูกตรวจสอบซ้ำ ๆ ด้วย switch หรือ if ที่ต่อกันเป็นลูกโซ่ซึ่งเลือก behavior ปัญหาทบทวีคูณ switch เดียวกันบน Type Code เดียวกันมักจะปรากฏในหลาย method ดังนั้นทุกชนิดใหม่จึงหมายถึงการแก้ไขแต่ละ method และคอมไพเลอร์ก็ไม่ช่วยอะไรเมื่อคุณลืมไปสักอัน Polymorphism เปลี่ยนกิ่งที่กระจัดกระจายเหล่านั้นให้เป็น dispatch เดียวที่ภาษาทำให้คุณ
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”Employee ที่ค่าจ้างขึ้นอยู่กับ string type ซึ่งถูกตรวจสอบด้วย switch หลังจากนั้น พนักงานแต่ละชนิดเป็น Subclass ที่รู้กฎค่าจ้างของตัวเอง
// Beforeclass Employee { constructor(public type: string, public base: number) {} payAmount(): number { switch (this.type) { case 'engineer': return this.base; case 'manager': return this.base * 1.2; case 'sales': return this.base + 500; default: throw new Error(`unknown type ${this.type}`); } }}
// Afterabstract class Employee { constructor(public base: number) {} abstract payAmount(): number;}class Engineer extends Employee { payAmount(): number { return this.base; }}class Manager extends Employee { payAmount(): number { return this.base * 1.2; }}class Salesperson extends Employee { payAmount(): number { return this.base + 500; }}# Beforeclass Employee: def __init__(self, kind, base): self.kind = kind self.base = base def pay_amount(self): if self.kind == "engineer": return self.base elif self.kind == "manager": return self.base * 1.2 elif self.kind == "sales": return self.base + 500 raise ValueError(f"unknown type {self.kind}")
# Afterfrom abc import ABC, abstractmethod
class Employee(ABC): def __init__(self, base): self.base = base @abstractmethod def pay_amount(self): ...
class Engineer(Employee): def pay_amount(self): return self.base
class Manager(Employee): def pay_amount(self): return self.base * 1.2
class Salesperson(Employee): def pay_amount(self): return self.base + 500// Beforetype Employee struct { Kind string Base float64}func (e Employee) PayAmount() float64 { switch e.Kind { case "engineer": return e.Base case "manager": return e.Base * 1.2 case "sales": return e.Base + 500 default: panic("unknown type " + e.Kind) }}
// Aftertype Employee interface { PayAmount() float64}
type Engineer struct{ Base float64 }func (e Engineer) PayAmount() float64 { return e.Base }
type Manager struct{ Base float64 }func (m Manager) PayAmount() float64 { return m.Base * 1.2 }
type Salesperson struct{ Base float64 }func (s Salesperson) PayAmount() float64 { return s.Base + 500 }// Beforestruct Employee { kind: String, base: f64,}impl Employee { fn pay_amount(&self) -> f64 { match self.kind.as_str() { "engineer" => self.base, "manager" => self.base * 1.2, "sales" => self.base + 500.0, other => panic!("unknown type {other}"), } }}
// Aftertrait Employee { fn pay_amount(&self) -> f64;}
struct Engineer { base: f64 }impl Employee for Engineer { fn pay_amount(&self) -> f64 { self.base }}
struct Manager { base: f64 }impl Employee for Manager { fn pay_amount(&self) -> f64 { self.base * 1.2 }}
struct Salesperson { base: f64 }impl Employee for Salesperson { fn pay_amount(&self) -> f64 { self.base + 500.0 }}flowchart TD
subgraph Before["Before — a type code"]
A["Employee<br/>type: 'engineer' | 'manager' | 'sales'"]
A --> B{"switch type"}
B --> C["pay logic for engineer"]
B --> D["pay logic for manager"]
B --> E["pay logic for sales"]
end
subgraph After["After — a hierarchy"]
F["Employee (abstract)<br/>payAmount()"]
F --> G["Engineer"]
F --> H["Manager"]
F --> I["Salesperson"]
end
Before -.->|"Replace Type Code with Subclasses"| After กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- ถ้า Type Code ยังไม่ถูกห่อหุ้ม ให้ซ่อนไว้หลัง accessor ก่อน caller จะได้ไม่พึ่ง field ดิบ
- สร้าง Subclass สำหรับค่าหนึ่งค่าของ Type Code จัดเตรียม factory (หรือ constructor) ที่คืน Subclass ที่ถูกต้องสำหรับ Code ที่กำหนด เพื่อให้การสร้างอยู่ในจุดเดียว
- ย้าย behavior สำหรับค่านั้น — กิ่งหนึ่งของ
switch— เข้าไปเป็น method ที่ override บน Subclass - รัน test
- ทำซ้ำสำหรับค่า Type Code ที่เหลือแต่ละค่า โดยลบแต่ละกิ่งออกจากเงื่อนไขเดิมไปเรื่อย ๆ
- เมื่อ branch สุดท้ายหายไป ให้ลบ
switchที่ว่างเปล่าออก และถ้าไม่มีใครต้องใช้แล้ว ก็ลบ field Type Code ทิ้งด้วย ตอนนี้ operation ฐานกลายเป็น abstract ที่ subclass ทุกตัวเป็นคน implement
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”ใช้ Subclasses เมื่อชนิดถูกตรึงไว้ตลอดอายุของ object และเมื่อมีหลาย method ที่แตกกิ่งบน Code เดียวกัน Polymorphism ยุบกิ่งเหล่านั้นและให้คอมไพเลอร์รับประกันว่าทุก Subclass อิมพลีเมนต์การทำงาน ดังนั้นเคสที่ลืมไปจึงกลายเป็น error ตอน build แทนที่จะเป็นเรื่องเซอร์ไพรส์ตอน runtime
แต่ Subclasses ไม่ใช่เครื่องมือที่ถูกต้องเสมอไป หากชนิดของ object เปลี่ยนระหว่างอายุ — ออเดอร์ที่ขยับจาก “pending” ไป “shipped” ไป “delivered” — คุณไม่สามารถสลับ class ได้ ดังนั้นจึงต้องหันไปใช้แพตเทิร์น State: ถือ object สถานะแยกต่างหากที่เอนทิตีสามารถแทนที่ได้เมื่อเปลี่ยนผ่าน และหากคุณต้องการแค่ให้อัลกอริทึมหนึ่งแปรเปลี่ยน แทนที่จะเป็น behavior ทั้งตระกูล Strategy — การฉีด objectbehavior เข้าไป — เบากว่าลำดับชั้นเต็มรูปแบบ เลือก Subclasses สำหรับตัวตนที่คงที่ State สำหรับตัวตนที่เปลี่ยนแปลง และ Strategy สำหรับอัลกอริทึมเดี่ยวที่สลับได้
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Replace Type Code with Subclasses เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| แต่ละ type มี behavior ที่แตกต่างกันชัดเจน | type เปลี่ยนได้ใน runtime — ใช้ State pattern แทน |
| ต้องการ polymorphism แทน switch statement | object ต้องการเป็นหลาย type พร้อมกัน |
| type ใหม่จะเพิ่มขึ้นบ่อย | subclass hierarchy จะ combine กับ hierarchy อื่น |
⚠️ ไม่ควร Replace Type Code with Subclasses เมื่อ:
- type เปลี่ยนได้หลัง object ถูกสร้าง — ต้องใช้ State แทน
- subclass จะมีแค่ data ต่างกัน ไม่มี behavior ต่างกัน — ใช้ parameter แทน
- สร้าง class hierarchy ที่ลึกเกินไป ทำให้ navigate ยาก