Replace Nested Conditional with Guard Clauses
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”เอา function ที่งานจริง ๆ ฝังอยู่ใต้บันได if/else มาดึงเคสพิเศษแต่ละเคสขึ้นไปไว้ด้านบนในรูป guard clause คือ check ที่ return ออกไปทันที พอทุกเคสยกเว้นออกไปก่อนหมดแล้ว เส้นทางปกติจะเหลืออยู่ที่ระดับย่อหน้าฐาน และอ่านได้เหมือนเป็นจุดประสงค์ที่แท้จริงของ function
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”นี่คือวิธีแก้อาการ เงื่อนไขซ้อนกัน ที่ทำให้หา happy path ไม่เจอ พอ if/else ซ้อนลึกสามสี่ชั้น บรรทัดที่คุณสนใจจริง ๆ จะร่นไปไกลทางขวา แถมต้องจำเงื่อนไขที่ห่ออยู่ทุกชั้นไว้ในหัวเพื่อจะรู้ว่ารันตอนไหน guard clause พลิกการเน้นเสียใหม่ เพราะการ return ออกไปก่อนแต่ละครั้งประกาศว่า “เคสประหลาดนี้จัดการแล้ว ปล่อยผ่านไปได้” พอไล่มาถึงด้านล่างจึงเหลือแค่สถานการณ์หลักล้วน ๆ
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”function เงินเดือน return จำนวนเงินที่ต่างกันสำหรับพนักงานที่เสียชีวิต ออกจากงาน และเกษียณ โดยมีเงินเดือนปกติเป็นค่าดีฟอลต์ ก่อนหน้านี้ กรณีต่าง ๆ ซ้อนกัน หลังจากนั้น แต่ละกรณีเป็น guard
// Beforefunction 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;}
// Afterfunction payAmount(employee: Employee): number { if (employee.isDead) return deadAmount(); if (employee.isSeparated) return separatedAmount(); if (employee.isRetired) return retiredAmount(); return normalPay(employee);}# Beforedef pay_amount(employee): if employee.is_dead: result = dead_amount() else: if employee.is_separated: result = separated_amount() else: if employee.is_retired: result = retired_amount() else: result = normal_pay(employee) return result
# Afterdef pay_amount(employee): if employee.is_dead: return dead_amount() if employee.is_separated: return separated_amount() if employee.is_retired: return retired_amount() return normal_pay(employee)// Beforefunc PayAmount(employee Employee) int { var result int if employee.IsDead { result = deadAmount() } else { if employee.IsSeparated { result = separatedAmount() } else { if employee.IsRetired { result = retiredAmount() } else { result = normalPay(employee) } } } return result}
// Afterfunc PayAmount(employee Employee) int { if employee.IsDead { return deadAmount() } if employee.IsSeparated { return separatedAmount() } if employee.IsRetired { return retiredAmount() } return normalPay(employee)}// Beforefn pay_amount(employee: &Employee) -> i64 { let result; if employee.is_dead { result = dead_amount(); } else { if employee.is_separated { result = separated_amount(); } else { if employee.is_retired { result = retired_amount(); } else { result = normal_pay(employee); } } } result}
// Afterfn pay_amount(employee: &Employee) -> i64 { if employee.is_dead { return dead_amount(); } if employee.is_separated { return separated_amount(); } if employee.is_retired { return retired_amount(); } normal_pay(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 กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- เลือกเงื่อนไขชั้นนอกสุดที่จัดการเคสพิเศษ เปลี่ยน branch นั้นเป็นการ return ออกไปก่อน แล้วยกขึ้นไปไว้บนสุดของ function
- รัน test ผลลัพธ์สำหรับกรณีนั้นต้องเหมือนเดิม
- ขยับไปยังเงื่อนไขชั้นนอกถัดไปและทำแบบเดียวกัน ลบ
elseออกได้เพราะกรณีด้านบน return ไปแล้ว - ทำต่อไปจนทุกกรณีพิเศษเป็น guard และเหลือเพียงเส้นทางหลักที่ด้านล่าง แบบไม่ย่อหน้า
- ถ้าเงื่อนไขของ 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 ก่อน