Switch Statements
แตกพฤติกรรมตามชนิด/สถานะด้วย switch ที่ทำซ้ำหลายที่
กลิ่นนี้คืออะไร
หัวข้อที่มีชื่อว่า “กลิ่นนี้คืออะไร”Switch Statements เกิดเมื่อ code ใช้ switch (หรือ if-else ต่อกันยาว ๆ) เพื่อ แตกพฤติกรรมตามชนิด สถานะ หรือ kind ของ object — เช่นถามว่า “นี่เป็นนกยุโรปหรือแอฟริกา” แล้วเลือกทำสิ่งที่ต่างกัน
switch เดียวเดี่ยว ๆ ไม่ใช่ปัญหา ปัญหาเกิดเมื่อ switch เดียวกันบนตัวแปรเดียวกันถูกก็อปวางซ้ำในหลายที่ทั่วโปรแกรม — ทุกครั้งที่มีการเพิ่มชนิดใหม่ (bird type ใหม่, order status ใหม่) นักพัฒนาต้องไล่หาทุก switch ที่แตก branch ตามชนิดนั้นแล้วแก้พร้อมกันให้ครบ ไม่งั้นจะมี branch ที่ตกหล่นแอบซ่อนเป็น bug
refactoring.guru และ SourceMaking สรุปสั้น ๆ ว่า “relatively rare use of switch and case operators is one of the hallmarks of object-oriented code” — พูดอีกแบบคือ เมื่อเห็น switch ควรคิดถึง polymorphism code OO ที่ดีมักไม่มี switch กระจายอยู่ทั่วไป เพราะงานแตก branch ตามชนิดถูกย้ายไปให้แต่ละ subclass รับผิดชอบตัวเอง
ข้อยกเว้นที่ยอมรับได้: switch ที่อยู่ จุดเดียว ภายใน factory method (เพื่อสร้าง object ที่ถูกชนิด) ไม่ถือเป็นกลิ่น — นั่นคือหน้าที่ปกติของ Factory Method หรือ Abstract Factory
วิธีสังเกต
หัวข้อที่มีชื่อว่า “วิธีสังเกต”- ค้นหา
switchหรือif...else if...else if...ที่แตกตาม type code, enum, หรือ property ที่บอกชนิด/สถานะของ object เดียวกัน - switch แบบเดียวกัน (บน field เดียวกัน) ปรากฏซ้ำในหลาย method หลาย class หรือหลาย file
- เวลาต้องเพิ่มชนิดใหม่หนึ่งชนิด ต้องแก้ file หลาย file ที่ไม่เกี่ยวข้องกันโดยตรง (อาการนี้คือ Shotgun Surgery)
- มี
default:ที่ throw exception หรือ log ว่า “unknown type” เพราะไม่มีใครมั่นใจว่า branch ครบทุกกรณีหรือไม่ - ตัวแปรที่ใช้แตก switch เป็น type code แบบ string/int/enum ธรรมดา ไม่ใช่ object ที่มีพฤติกรรมในตัว (มักไปคู่กับ Primitive Obsession)
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”ผิดหลัก Open-Closed — ทุกครั้งที่มีชนิดใหม่ ต้องเปิด code เดิมที่ทำงานได้อยู่แล้วมาแก้ ขัดกับ Open-Closed Principle ที่ต้องการให้ระบบขยายได้โดยไม่แก้ของเดิม
ทำซ้ำและกระจาย — switch เดียวกันที่ก็อปวางหลายจุดคือ Duplicate Code เต็มรูปแบบ การแก้ไขหนึ่งจุดไม่ได้แปลว่าอีกจุดจะถูกแก้ตาม เสี่ยงต่อ Shotgun Surgery ทุกครั้งที่ requirement เปลี่ยน
เสี่ยงตกหล่น branch — ไม่มี compiler ตัวไหนบังคับว่า switch ทุกจุดต้องมี case ครบตามชนิดที่มีอยู่จริง เพิ่ม enum ใหม่แล้วลืมแก้ switch สักจุดหนึ่ง โปรแกรมจะพังแบบเงียบ ๆ ที่ runtime (ตกไปที่ default หรือ throw)
ปนตรรกะ + สร้างกลิ่นอื่นตามมา — method ที่มี switch ยาว ๆ มักโตจนกลายเป็น Long Method และเปิดช่องให้แปะ Comments อธิบายแต่ละ case แทนที่จะให้ชื่อ class/method สื่อความหมายเอง
ทดสอบยาก — test ต้อง cover ทุก branch ในทุกจุดที่ก็อป switch ไว้ ยิ่งจุดเยอะยิ่ง test ทับซ้อนและเปราะ
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”ตัวอย่างคลาสสิกจาก Martin Fowler’s Refactoring catalog: คำนวณคำอธิบายขนสัตว์ (plumage) ของนกตามชนิด
code ก่อน (มี switch แตกตาม type code และมักถูกก็อปไปคำนวณ numberOfCoconuts หรือ airSpeedVelocity ที่อื่นซ้ำอีก):
public string GetPlumage(Bird bird){ switch (bird.Type) { case BirdType.EuropeanSwallow: return "average"; case BirdType.AfricanSwallow: return bird.NumberOfCoconuts > 2 ? "tired" : "average"; case BirdType.NorwegianBlueParrot: return bird.Voltage > 100 ? "scorched" : "beautiful"; default: throw new ArgumentException("unknown bird type"); }}ปัญหา: ถ้ามี method GetAirSpeedVelocity(bird) อีกตัวที่ต้องแตกตาม bird.Type เหมือนกัน code switch ชุดนี้จะถูกก็อปซ้ำ และถ้าเพิ่ม BirdType.IndianRingneckParrot วันหนึ่ง ต้องไล่แก้ทุก method ที่มี switch บน BirdType
refactor ด้วย Replace Conditional with Polymorphism (Martin Fowler) — ยก code ของแต่ละ branch ไปเป็น method ใน subclass ของมันเอง แล้วให้ runtime polymorphism เลือก implementation แทนการ if/switch:
public abstract class Bird{ public abstract string Plumage { get; }}
public class EuropeanSwallow : Bird{ public override string Plumage => "average";}
public class AfricanSwallow : Bird{ public int NumberOfCoconuts { get; set; }
public override string Plumage => NumberOfCoconuts > 2 ? "tired" : "average";}
public class NorwegianBlueParrot : Bird{ public int Voltage { get; set; }
public override string Plumage => Voltage > 100 ? "scorched" : "beautiful";}ตอนเรียกใช้ ไม่ต้อง switch อีกต่อไป:
string description = bird.Plumage;เพิ่มนกชนิดใหม่ก็แค่เพิ่ม subclass ใหม่หนึ่งตัว โดยไม่ต้องแตะ code เดิมเลย — ตรงตาม Open-Closed และแต่ละ subclass ยัง swap แทนกันได้ตาม Liskov Substitution เพราะทุกตัวสัญญาแค่ Plumage เหมือนกัน
เมื่อ “ชนิด” เปลี่ยนแปลงระหว่างการทำงาน (เช่น order ที่เปลี่ยนสถานะ pending → shipped → delivered) ให้ใช้ State แทนการสร้าง subclass ตายตัว — object เปลี่ยน “ชนิดพฤติกรรม” ของตัวเองได้โดยสลับ state object ภายใน
เมื่อ switch เลือก algorithm/พฤติกรรมที่ผูกกับ context ภายนอก (ไม่ใช่ตัว object เอง) ให้ใช้ Strategy แทน
เมื่อหนึ่งใน branch คือ “ไม่มีอะไรทำ” หรือเช็ค null ให้ใช้ Null Object แทน if (obj == null) ที่กระจายซ้ำ
แผนภาพเปรียบเทียบโครงสร้างก่อน/หลัง:
flowchart TB
subgraph Before
Client1[Client] --> Switch[GetPlumage switch on Type]
Switch --> A1[case EuropeanSwallow]
Switch --> A2[case AfricanSwallow]
Switch --> A3[case NorwegianBlueParrot]
end
subgraph After
Client2[Client] --> Base[Bird.Plumage]
Base --> B1[EuropeanSwallow.Plumage]
Base --> B2[AfricanSwallow.Plumage]
Base --> B3[NorwegianBlueParrot.Plumage]
end
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Open-Closed Principle — เหตุผลหลักที่ switch กระจายเป็นปัญหา
- Liskov Substitution — สัญญาที่ subclass ที่แทน switch ต้องรักษาไว้
- Strategy — แทน switch เมื่อ “พฤติกรรม” ผูกกับ context ภายนอก
- State — แทน switch เมื่อ “ชนิด/สถานะ” เปลี่ยนระหว่างการทำงาน
- Null Object — แทน branch ที่เช็ค null ซ้ำ ๆ
- Shotgun Surgery — อาการที่ตามมาเมื่อ switch เดียวกันกระจายหลายจุด