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

Replace Nested Conditional with Guard Clauses

เอา function ที่งานจริง ๆ ฝังอยู่ใต้บันได if/else มาดึงเคสพิเศษแต่ละเคสขึ้นไปไว้ด้านบนในรูป guard clause คือ check ที่ return ออกไปทันที พอทุกเคสยกเว้นออกไปก่อนหมดแล้ว เส้นทางปกติจะเหลืออยู่ที่ระดับย่อหน้าฐาน และอ่านได้เหมือนเป็นจุดประสงค์ที่แท้จริงของ function

นี่คือวิธีแก้อาการ เงื่อนไขซ้อนกัน ที่ทำให้หา happy path ไม่เจอ พอ if/else ซ้อนลึกสามสี่ชั้น บรรทัดที่คุณสนใจจริง ๆ จะร่นไปไกลทางขวา แถมต้องจำเงื่อนไขที่ห่ออยู่ทุกชั้นไว้ในหัวเพื่อจะรู้ว่ารันตอนไหน guard clause พลิกการเน้นเสียใหม่ เพราะการ return ออกไปก่อนแต่ละครั้งประกาศว่า “เคสประหลาดนี้จัดการแล้ว ปล่อยผ่านไปได้” พอไล่มาถึงด้านล่างจึงเหลือแค่สถานการณ์หลักล้วน ๆ

function เงินเดือน return จำนวนเงินที่ต่างกันสำหรับพนักงานที่เสียชีวิต ออกจากงาน และเกษียณ โดยมีเงินเดือนปกติเป็นค่าดีฟอลต์ ก่อนหน้านี้ กรณีต่าง ๆ ซ้อนกัน หลังจากนั้น แต่ละกรณีเป็น guard

// Before
function payAmount(employee: Employee): number {
let result: number;
if (employee.isDead) {
result = deadAmount();
} else {
if (employee.isSeparated) {
result = separatedAmount();
} else {
if (employee.isRetired) {
result = retiredAmount();
} else {
result = normalPay(employee);
}
}
}
return result;
}
// After
function payAmount(employee: Employee): number {
if (employee.isDead) return deadAmount();
if (employee.isSeparated) return separatedAmount();
if (employee.isRetired) return retiredAmount();
return normalPay(employee);
}
flowchart TD
  subgraph Before["Before — nested"]
    A{"is dead?"} -->|no| B{"is separated?"}
    A -->|yes| AD["deadAmount"]
    B -->|no| C{"is retired?"}
    B -->|yes| BD["separatedAmount"]
    C -->|no| CN["normalPay"]
    C -->|yes| CD["retiredAmount"]
  end
  subgraph After["After — guards"]
    G1["if dead return deadAmount"] --> G2["if separated return separatedAmount"]
    G2 --> G3["if retired return retiredAmount"]
    G3 --> G4["return normalPay"]
  end
  Before -.->|"Replace Nested Conditional with Guard Clauses"| After
บันได if/else ที่ซ้อนกันกลายเป็นลำดับ guard clause ที่ราบเรียบ
  1. เลือกเงื่อนไขชั้นนอกสุดที่จัดการเคสพิเศษ เปลี่ยน branch นั้นเป็นการ return ออกไปก่อน แล้วยกขึ้นไปไว้บนสุดของ function
  2. รัน test ผลลัพธ์สำหรับกรณีนั้นต้องเหมือนเดิม
  3. ขยับไปยังเงื่อนไขชั้นนอกถัดไปและทำแบบเดียวกัน ลบ else ออกได้เพราะกรณีด้านบน return ไปแล้ว
  4. ทำต่อไปจนทุกกรณีพิเศษเป็น guard และเหลือเพียงเส้นทางหลักที่ด้านล่าง แบบไม่ย่อหน้า
  5. ถ้าเงื่อนไขของ guard อ่านขัด ๆ ตอนกลับด้าน ให้ลอง extract ออกมาด้วย Decompose Conditional การ return ก่อนจะได้ยังอ่านสะอาด

หยิบ guard clause มาใช้เมื่อการซ้อนกันบดบังการไหลปกติของ function โดยเฉพาะเมื่อสาขาหลายตัวเป็น ข้อยกเว้น มากกว่าเป็นทางเลือกที่เท่าเทียมกัน การ refactor นี้โดดเด่นเมื่อกรณีต่าง ๆ เป็นแบบ “จัดการแล้วจากไป” จริง ๆ — การตรวจสอบข้อผิดพลาด ข้อมูลที่หายไป สถานะขอบเขต

ท่านี้เหมาะน้อยลงเมื่อ branch ทั้งหลายเป็นทางเลือกคู่ขนานที่มีน้ำหนักเท่ากันจริง ๆ การฝืนยัดให้เป็น guard จะสื่อลำดับความสำคัญที่ไม่มีอยู่จริง บางทีมยังยึดหลักหนึ่ง function หนึ่งจุดออกด้วย ถ้าทีมคุณเป็นแบบนั้นก็ต้องชั่งธรรมเนียมนั้นกับความอ่านง่ายที่จะได้ แต่ใน code สมัยใหม่ส่วนใหญ่ เวอร์ชันที่แบนราบกว่าชนะ

ใช้ Guard Clauses เมื่อหลีกเลี่ยงเมื่อ
เส้นทางหลักฝังอยู่ใน else ที่ซ้อนลึกสาขาทั้งหมดมีน้ำหนักเท่ากัน ไม่มีตัวใด “ข้อยกเว้น”
กรณีต่าง ๆ เป็น error/edge case ที่จัดการแล้วออกไปteam บังคับ single exit point ต่อ function
การซ้อนทำให้ happy path อ่านยากมากguard ใหม่จะทำให้มี exit points กระจายมากเกินไป

⚠️ ไม่ควร Replace Nested Conditional with Guard Clauses เมื่อ:

  • สาขาล้วนเป็นทางเลือกที่เท่าเทียมกัน — guard จะสื่อว่าบางอันสำคัญน้อยกว่าโดยไม่ตั้งใจ
  • body ของ loop ยังซับซ้อน — ให้ flatten ด้วย Extract Function ก่อน
  • เงื่อนไขของ guard อ่านขัดเขินเมื่อกลับด้าน — ให้ Decompose Conditional ก่อน
guard clause คืออะไร?
หลังจากแทนเงื่อนไขที่ซ้อนกันด้วย guard แล้ว เส้นทางหลักไปอยู่ที่ไหน?
เมื่อไรที่ guard clause เหมาะสมน้อยลง?
คุณควรรันอะไรหลังจากยกแต่ละกรณีพิเศษขึ้นไปเป็น guard?