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

Decompose Conditional

เอาเงื่อนไขที่ทั้ง การทดสอบ และ สาขา ต่างเป็นก้อนรายละเอียด แล้วแทนแต่ละก้อนด้วยการเรียก function ที่ตั้งชื่อตามเจตนา จากนั้น if ก็จะอ่านได้เหมือนประโยค — ถ้าไม่ใช่หน้าร้อน ให้ใช้ค่าธรรมเนียมฤดูหนาว มิฉะนั้นใช้ค่าธรรมเนียมฤดูร้อน — และการคำนวณที่รกรุงรังก็ถูกย้ายไปพ้นทาง

นี่คือวิธีแก้สำหรับ เงื่อนไขที่ซับซ้อน — ชนิดที่คุณบอกไม่ได้เลยว่าทำไมสาขาหนึ่งถึงถูกเลือก ถ้าไม่ได้แกะ expression ออกมาดู การทดสอบเป็นการเปรียบเทียบวันที่ที่พันกัน ส่วนสาขาก็เป็นสูตรที่เต็มไปด้วยตัวเลขวิเศษ ลำดับการควบคุมที่คุณสนใจ (กรณีไหนใช้ได้) จมหายไปในการคำนวณ เมื่อเงื่อนไขและแต่ละสาขามีชื่อเป็นของตัวเอง การตัดสินใจก็จะชัดเจน และคณิตศาสตร์ก็ถูกซุกไว้ในตัวช่วยที่คุณอ่านได้เมื่อต้องการ

function คิดค่าบริการคิดอัตราฤดูหนาวนอกหน้าร้อน และอัตราฤดูร้อนภายในหน้าร้อน ก่อนหน้านี้ การทดสอบและสาขาทั้งสองเป็นการคำนวณแบบ inline หลังจากนั้น แต่ละส่วนกลายเป็นตัวช่วยที่มีชื่อ

// Before
function charge(date: Date, quantity: number): number {
let result: number;
if (date.getMonth() < 5 || date.getMonth() > 8) {
result = quantity * 12 + 100;
} else {
result = quantity * 8;
}
return result;
}
// After
function charge(date: Date, quantity: number): number {
return notSummer(date) ? winterCharge(quantity) : summerCharge(quantity);
}
function notSummer(date: Date): boolean {
return date.getMonth() < 5 || date.getMonth() > 8;
}
function winterCharge(quantity: number): number {
return quantity * 12 + 100;
}
function summerCharge(quantity: number): number {
return quantity * 8;
}
flowchart LR
  subgraph Before["Before"]
    A["if (date before summer<br/>or date after summer)<br/>charge = qty * winterRate + flat<br/>else<br/>charge = qty * summerRate"]
  end
  subgraph After["After"]
    B["if notSummer(date)"]
    B --> C["winterCharge(qty)"]
    B --> D["summerCharge(qty)"]
  end
  Before -.->|"Decompose Conditional"| After
if ที่พันกันกลายเป็นการทดสอบที่มีชื่อบวกกับสองสาขาที่มีชื่อ
  1. ใช้ Extract Function กับเงื่อนไข: ยกการทดสอบ boolean ทั้งก้อนออกมาเป็น function ที่ชื่อตอบคำถาม ทำไม สาขานี้ถึงถูกเลือก (notSummer) ไม่ใช่ อย่างไร ที่ถูกคำนวณ
  2. ใช้ Extract Function กับสาขา then ตั้งชื่อตามผลลัพธ์ที่สร้าง (winterCharge)
  3. ใช้ Extract Function กับสาขา else ในแบบเดียวกัน (summerCharge)
  4. รัน test หลังการสกัดแต่ละครั้ง behavior ต้องไม่เปลี่ยนแปลง
  5. เมื่อทั้งสามส่วนมีชื่อแล้ว if เดิมก็สั้นพอที่จะปล่อยไว้ตามเดิม หรือยุบให้เป็น expression เดียวได้หากภาษาของคุณมี expression เงื่อนไขที่สะอาด

หยิบ Decompose Conditional มาใช้ทุกครั้งที่ เงื่อนไข ของ if อ่านยาก หรือเมื่อ branch ใด branch หนึ่งเป็นก้อนคำนวณที่บังการตัดสินใจไว้ ท่านี้เข้าคู่กับ Extract Function อย่างเป็นธรรมชาติ เพราะต้องพึ่งเครื่องมือตัวนั้นถึงสามรอบ

ต้นทุนก็เหมือนการสกัดทั่วไป: สามก้าวสั้น ๆ จาก if ไปยังตัวช่วย นั่นแทบจะคุ้มค่าเสมอ เพราะผู้อ่านที่กวาดสายตาหาลำดับการควบคุมตอนนี้สามารถอ่านการตัดสินใจได้โดยไม่ต้องดำดิ่งลงไปในการคำนวณ หากสาขาหนึ่งเป็น expression ที่ง่ายเล็กน้อยอยู่แล้ว ก็ปล่อยไว้แบบ inline — การตั้งชื่อ quantity * 8 ไม่ได้เพิ่มอะไรเลย

ใช้ Decompose Conditional เมื่อหลีกเลี่ยงเมื่อ
เงื่อนไขต้องแกะ logic เพื่อเข้าใจว่าถามว่าอะไรเงื่อนไขสั้น ๆ ที่ชัดเจนอยู่แล้ว
สาขา then/else เป็นการคำนวณที่ซับซ้อนตั้งชื่อที่ดีกว่าเดิมไม่ได้ — แสดงว่า logic ยังไม่ชัด
ต้องการให้ผู้อ่านเห็น decision โดยไม่ดำดิ่งรายละเอียดfunction ที่ extract ออกมาถูกใช้ที่เดียวและเพิ่ม indirection ฟรี ๆ

⚠️ ไม่ควร Decompose Conditional เมื่อ:

  • เงื่อนไขนั้นง่ายและชัดเจน — เช่น if (x > 0) ไม่จำเป็นต้องตั้งชื่อเพิ่ม
  • สาขาที่จะ extract มีแค่ 1-2 บรรทัดและชื่อไม่ได้อธิบายเพิ่มขึ้น
  • ยังไม่มี test — extract โดยไม่มี safety net เพิ่มความเสี่ยง
Decompose Conditional แยกสามส่วนใดของเงื่อนไขออกมา?
function เงื่อนไขที่สกัดออกมาควรตั้งชื่อตามอะไร?
Decompose Conditional พึ่งพาการ refactor ที่มีอยู่แล้วตัวใด?
เมื่อไรที่คุณควรปล่อยสาขาไว้แบบ inline แทนที่จะสกัดออกมา?