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

Replace Conditional with Polymorphism

เอา switch (หรือโซ่ if/else) ที่แตกสาขาตาม ชนิด ของค่า แล้วให้แต่ละชนิดมี class ของตัวเองพร้อม method เวอร์ชันของตัวเอง ป้ายสาขากลายเป็น subclass เนื้อในของแต่ละ case กลายเป็น method ที่ override และการกระจายงาน (dispatch) ที่เคยเขียนด้วยมือก็ถูกภาษาทำให้ ณ จุดที่เรียก

นี่คือวิธีแก้อาการ type switch ที่เกิดซ้ำ สัญญาณไม่ใช่การมี switch ตัวเดียว แต่คือ switch ชุดเดียวกัน บน field ชนิดเดียวกัน โผล่ซ้ำในหลาย function เช่น plumage, airSpeed, singing ที่ต่างก็ไล่ระบุนกทุกชนิดใหม่หมด ทุกครั้งที่เพิ่มชนิดนก คุณต้องไล่หาและแก้ให้ครบทุกจุด แล้วก็พลาดไปสักจุดได้ง่ายมาก polymorphism รวบทุกอย่างที่ชนิดหนึ่งทำไว้ใน class เดียว การเพิ่มชนิดใหม่จึงหมายถึงการเพิ่ม class ไม่ใช่การไล่ล่า case ที่กระจัดกระจาย

ขนนกของนกขึ้นอยู่กับสายพันธุ์ ก่อนหน้านี้ switch ตัวเดียวบนป้ายชนิดจัดการทุกสายพันธุ์ หลังจากนั้น แต่ละสายพันธุ์เป็น subclass ที่ override plumage

// Before
function 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';
}
}
// After
abstract 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';
}
}
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
type switch ที่เกิดซ้ำกลายเป็นชนิดฐานและ subclass หนึ่งตัวต่อหนึ่ง case
  1. สร้างชนิดฐาน (class, interface, หรือ trait) พร้อม method ที่ switch คำนวณอยู่ในปัจจุบัน
  2. สร้าง subclass หนึ่งตัวต่อหนึ่งป้ายสาขา ย้ายเนื้อในของแต่ละ case เข้าไปใน override ของ subclass นั้น โดยแทนการอ้างอิง field ชนิดด้วยข้อมูลของ subclass เอง
  3. แทนจุดที่สร้าง object เพื่อให้ subclass ที่ถูกต้องถูกสร้างขึ้นแทนค่าที่มีป้าย — มักผ่าน factory เล็ก ๆ
  4. เปลี่ยน switch เดิมให้เป็นการเรียก method แบบ polymorphic เพียงครั้งเดียว รัน test
  5. ทำซ้ำสำหรับ 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 เดิมกระจายซ้ำในหลาย functionswitch ตัวเดียว ใน 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 ธรรมดา
อะไรคือสัญญาณที่ชัดเจนที่สุดว่าคุณควรหยิบ Replace Conditional with Polymorphism มาใช้?
ป้ายสาขาของ switch กลายเป็นอะไร?
หลังจากย้าย logic เข้าไปใน subclass แล้ว อะไรมาแทน switch เดิม?
เมื่อไรที่การ refactor นี้เกินความจำเป็น?