Replace Conditional with Polymorphism
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”เอา switch (หรือโซ่ if/else) ที่แตกสาขาตาม ชนิด ของค่า แล้วให้แต่ละชนิดมี class ของตัวเองพร้อม method เวอร์ชันของตัวเอง ป้ายสาขากลายเป็น subclass เนื้อในของแต่ละ case กลายเป็น method ที่ override และการกระจายงาน (dispatch) ที่เคยเขียนด้วยมือก็ถูกภาษาทำให้ ณ จุดที่เรียก
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”นี่คือวิธีแก้อาการ type switch ที่เกิดซ้ำ สัญญาณไม่ใช่การมี switch ตัวเดียว แต่คือ switch ชุดเดียวกัน บน field ชนิดเดียวกัน โผล่ซ้ำในหลาย function เช่น plumage, airSpeed, singing ที่ต่างก็ไล่ระบุนกทุกชนิดใหม่หมด ทุกครั้งที่เพิ่มชนิดนก คุณต้องไล่หาและแก้ให้ครบทุกจุด แล้วก็พลาดไปสักจุดได้ง่ายมาก polymorphism รวบทุกอย่างที่ชนิดหนึ่งทำไว้ใน class เดียว การเพิ่มชนิดใหม่จึงหมายถึงการเพิ่ม class ไม่ใช่การไล่ล่า case ที่กระจัดกระจาย
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”ขนนกของนกขึ้นอยู่กับสายพันธุ์ ก่อนหน้านี้ switch ตัวเดียวบนป้ายชนิดจัดการทุกสายพันธุ์ หลังจากนั้น แต่ละสายพันธุ์เป็น subclass ที่ override plumage
// Beforefunction plumage(bird: Bird): string { switch (bird.type) { case 'EuropeanSwallow': return 'average'; case 'AfricanSwallow': return bird.numberOfCoconuts > 2 ? 'tired' : 'average'; case 'NorwegianBlue': return bird.voltage > 100 ? 'scorched' : 'beautiful'; default: return 'unknown'; }}
// Afterabstract class Bird { abstract plumage(): string;}
class EuropeanSwallow extends Bird { plumage(): string { return 'average'; }}
class AfricanSwallow extends Bird { constructor(private numberOfCoconuts: number) { super(); } plumage(): string { return this.numberOfCoconuts > 2 ? 'tired' : 'average'; }}
class NorwegianBlue extends Bird { constructor(private voltage: number) { super(); } plumage(): string { return this.voltage > 100 ? 'scorched' : 'beautiful'; }}# Beforedef plumage(bird): if bird.type == "EuropeanSwallow": return "average" elif bird.type == "AfricanSwallow": return "tired" if bird.number_of_coconuts > 2 else "average" elif bird.type == "NorwegianBlue": return "scorched" if bird.voltage > 100 else "beautiful" return "unknown"
# Afterclass Bird: def plumage(self): raise NotImplementedError
class EuropeanSwallow(Bird): def plumage(self): return "average"
class AfricanSwallow(Bird): def __init__(self, number_of_coconuts): self.number_of_coconuts = number_of_coconuts
def plumage(self): return "tired" if self.number_of_coconuts > 2 else "average"
class NorwegianBlue(Bird): def __init__(self, voltage): self.voltage = voltage
def plumage(self): return "scorched" if self.voltage > 100 else "beautiful"// Beforefunc Plumage(bird Bird) string { switch bird.Type { case "EuropeanSwallow": return "average" case "AfricanSwallow": if bird.NumberOfCoconuts > 2 { return "tired" } return "average" case "NorwegianBlue": if bird.Voltage > 100 { return "scorched" } return "beautiful" default: return "unknown" }}
// Aftertype Bird interface { Plumage() string}
type EuropeanSwallow struct{}
func (e EuropeanSwallow) Plumage() string { return "average" }
type AfricanSwallow struct{ NumberOfCoconuts int }
func (a AfricanSwallow) Plumage() string { if a.NumberOfCoconuts > 2 { return "tired" } return "average"}
type NorwegianBlue struct{ Voltage int }
func (n NorwegianBlue) Plumage() string { if n.Voltage > 100 { return "scorched" } return "beautiful"}// Beforefn plumage(bird: &Bird) -> String { match bird.kind.as_str() { "EuropeanSwallow" => "average".to_string(), "AfricanSwallow" => { if bird.number_of_coconuts > 2 { "tired" } else { "average" }.to_string() } "NorwegianBlue" => { if bird.voltage > 100 { "scorched" } else { "beautiful" }.to_string() } _ => "unknown".to_string(), }}
// Aftertrait Bird { fn plumage(&self) -> String;}
struct EuropeanSwallow;
impl Bird for EuropeanSwallow { fn plumage(&self) -> String { "average".to_string() }}
struct AfricanSwallow { number_of_coconuts: u32,}
impl Bird for AfricanSwallow { fn plumage(&self) -> String { if self.number_of_coconuts > 2 { "tired" } else { "average" }.to_string() }}
struct NorwegianBlue { voltage: u32,}
impl Bird for NorwegianBlue { fn plumage(&self) -> String { if self.voltage > 100 { "scorched" } else { "beautiful" }.to_string() }}flowchart TD
subgraph Before["Before — one switch, repeated"]
A["plumage(bird):<br/>switch bird.type<br/>case EuropeanSwallow ...<br/>case AfricanSwallow ...<br/>case NorwegianBlue ..."]
end
subgraph After["After — polymorphic classes"]
B["Bird.plumage()"]
B --> C["EuropeanSwallow.plumage()"]
B --> D["AfricanSwallow.plumage()"]
B --> E["NorwegianBlue.plumage()"]
end
Before -.->|"Replace Conditional with Polymorphism"| After กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- สร้างชนิดฐาน (class, interface, หรือ trait) พร้อม method ที่
switchคำนวณอยู่ในปัจจุบัน - สร้าง subclass หนึ่งตัวต่อหนึ่งป้ายสาขา ย้ายเนื้อในของแต่ละ case เข้าไปใน override ของ subclass นั้น โดยแทนการอ้างอิง field ชนิดด้วยข้อมูลของ subclass เอง
- แทนจุดที่สร้าง object เพื่อให้ subclass ที่ถูกต้องถูกสร้างขึ้นแทนค่าที่มีป้าย — มักผ่าน factory เล็ก ๆ
- เปลี่ยน
switchเดิมให้เป็นการเรียก method แบบ polymorphic เพียงครั้งเดียว รัน test - ทำซ้ำสำหรับ function ถัดไป ที่ switch บนชนิดเดียวกัน ลบ
switchแต่ละตัวทิ้งเมื่อ logic ย้ายเข้าไปใน subclass รัน test หลังจากทุก method ที่คุณย้าย
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”ใช้ท่านี้ อย่างประหยัด เพราะคุ้มก็ต่อเมื่อเงื่อนไข ชุดเดียวกัน บน type เดียวกันโผล่มากกว่าหนึ่งที่ จนการเพิ่มชนิดใหม่วันนี้แปลว่าต้องแก้หลาย function ความซ้ำตรงนั้นแหละคือสิ่งที่ class hierarchy เข้ามากำจัด
ต้นทุนมีจริง: คุณเพิ่มลำดับชั้น เพิ่ม factory และเพิ่มความอ้อม สำหรับ switch ตัวเดียวที่อยู่ใน function เดียว polymorphism เกินความจำเป็น — switch ธรรมดาชัดเจนกว่า และ Decompose Conditional หรือ guard clause ทำหน้าที่ได้ดีกว่า เพิ่ม class ก็ต่อเมื่อการเกิดซ้ำพิสูจน์แล้วว่าโครงสร้างนั้นคุ้มค่า
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Replace Conditional with Polymorphism เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| switch เดิมกระจายซ้ำในหลาย function | switch ตัวเดียว ใน function เดียว ไม่ซ้ำที่อื่น |
| การเพิ่ม type ใหม่ต้องแก้หลาย switch พร้อมกัน | type มีจำนวนน้อยและ stable ไม่เพิ่มบ่อย |
| behavior แต่ละ type ต่างกันชัดเจนและมีความซับซ้อน | ลำดับชั้น class เพิ่ม indirection โดยไม่ได้ลด switch จริง |
⚠️ ไม่ควร Replace Conditional with Polymorphism เมื่อ:
- switch นั้นตัวเดียวในโปรแกรมทั้งหมด — Decompose Conditional หรือ guard clause ง่ายกว่า
- type ถูก hardcode และไม่มีแผนเพิ่ม — overhead ของ class hierarchy ไม่คุ้ม
- team ที่ดูแล codebase ไม่คุ้นชิน pattern นี้ — อ่านยากกว่า switch ธรรมดา