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

Extract Function

ย้ายชิ้นส่วน code ที่เป็นเอกภาพเข้าไปใน function ใหม่ แล้วตั้งชื่อตาม จุดประสงค์ ที่ทำหน้าที่อยู่ จุดเดิมเหลือแค่การเรียก helper จึงอ่านได้เหมือนรายการสั้น ๆ ของขั้นตอนที่มีชื่อ แทนที่จะเป็นรายละเอียดพรืดเป็นพืด

นี่คือวิธีรักษา Long Function และรักษาบล็อก code ใด ๆ ที่ต้องมี comment มาอธิบายว่าทำอะไรอยู่ ลองนึกถึง comment ที่เขียนว่า “ตอนนี้คำนวณภาษี” ไว้เหนือ code สิบบรรทัด นั่นคือป้ายไฟกระพริบว่า code สิบบรรทัดนั้นอยากเป็น function ชื่อ taxFor พอชื่อ function แบกเจตนาไว้แล้ว comment ก็ไม่จำเป็นอีกต่อไป และจุดที่เรียกก็อ่านง่ายขึ้น

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

// Before
function printReceipt(items: { price: number; qty: number }[]): void {
let subtotal = 0;
for (const item of items) {
subtotal += item.price * item.qty;
}
const tax = subtotal * 0.07;
console.log(`Subtotal: ${subtotal.toFixed(2)}`);
console.log(`Tax: ${tax.toFixed(2)}`);
console.log(`Total: ${(subtotal + tax).toFixed(2)}`);
}
// After
function printReceipt(items: { price: number; qty: number }[]): void {
const sub = subtotal(items);
const tax = taxFor(sub);
printLines(sub, tax);
}
function subtotal(items: { price: number; qty: number }[]): number {
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
function taxFor(amount: number): number {
return amount * 0.07;
}
function printLines(sub: number, tax: number): void {
console.log(`Subtotal: ${sub.toFixed(2)}`);
console.log(`Tax: ${tax.toFixed(2)}`);
console.log(`Total: ${(sub + tax).toFixed(2)}`);
}
flowchart LR
  subgraph Before["Before"]
    A["printReceipt()<br/>— compute subtotal<br/>— compute tax<br/>— print lines"]
  end
  subgraph After["After"]
    B["printReceipt()<br/>calls helpers"]
    B --> C["subtotal()"]
    B --> D["taxFor()"]
    B --> E["printLines()"]
  end
  Before -.->|"Extract Function"| After
function ยาวหนึ่งตัว กลายเป็นตัวเรียก helper ที่มีชื่อสามตัว
  1. เลือกชิ้นส่วนที่จะ extract และตั้งชื่อที่บอกว่า ได้ผลลัพธ์อะไร ไม่ใช่บอกว่าทำอย่างไร ถ้าตั้งชื่อสะอาด ๆ ไม่ได้ แปลว่าชิ้นส่วนนั้นอาจยังไม่เป็นหน่วยเดียวกัน ลองขยับขอบเขตใหม่
  2. สร้าง function เปล่าที่ใช้ชื่อนั้น
  3. คัดลอกชิ้นส่วนเดิมเข้าไปใน function ใหม่
  4. ดูว่าชิ้นส่วนนั้นอ่านตัวแปรอะไรบ้าง แล้วส่งเข้าไปเป็น parameter ส่วนตัวแปรที่สร้างขึ้นใหม่และ code ข้างนอกยังต้องใช้ ก็คืนค่าออกมา
  5. แทนที่ชิ้นส่วนเดิมด้วยการเรียก function ใหม่
  6. รัน test behavior ต้องไม่เปลี่ยน
  7. ทำซ้ำกับชิ้นส่วนถัดไป extract ทีละนิดเข้าไว้ test ที่พังจะได้ชี้ไปที่การเปลี่ยนแปลงเล็ก ๆ จุดเดียว

หยิบ Extract Function มาใช้เมื่อบล็อกหนึ่งมีจุดประสงค์เดียวชัดเจน เมื่อคุณเริ่มอยากเขียน comment มาอธิบาย หรือเมื่อเห็นชิ้นส่วนเดียวกันเขียนซ้ำในสองที่ extract ครั้งเดียวแล้วให้ทั้งสองจุดเรียก helper ตัวเดียวกัน

ต้นทุนคือการกระโดดเล็ก ๆ ตอนอ่าน เพราะผู้อ่านต้องตามชื่อไปยังนิยามของ function นั้น แต่แลกแล้วคุ้มแทบทุกครั้ง เพราะชื่อที่ดีทำให้ผู้อ่านส่วนใหญ่หยุดได้เลย โดยไม่ต้อง กระโดดตามไป refactoring ฝั่งตรงข้ามคือ Inline Function ถ้าชื่อ helper บอกอะไรไม่มากไปกว่า body ก็พับกลับเข้าไปเลย

ใช้ Extract Function เมื่อหลีกเลี่ยงเมื่อ
บล็อกนั้นต้องมี comment อธิบายตั้งชื่อที่ดีให้ไม่ได้ — แปลว่ายังแบ่งไม่ถูกที่
logic เดิมปรากฏมากกว่าหนึ่งที่function มีแค่ 1-2 บรรทัดและชื่อไม่ได้อธิบายเพิ่ม
function ยาวจน scroll ไม่ถึงท้ายextract เพื่อสร้าง abstraction ทั้งที่ยังไม่รู้ว่าจะได้ใช้
test ล้มเหลวแต่ไม่รู้ว่า logic ไหนผิด

⚠️ ไม่ควร Extract Function เมื่อ:

  • ยังไม่มี test — refactor โดยไม่มี safety net คือการเดา
  • บล็อกนั้นใช้ตัวแปรจากบริบทภายนอกมากเกินสี่ตัว — อาจควร Extract Class แทน
  • กำลัง extract เพื่อให้ดูเหมือนทำงาน ไม่ใช่เพราะ code อ่านยากจริง ๆ
เมื่อเลือกชื่อให้ function ที่ extract ออกมา ชื่อนั้นควรอธิบายอะไร?
ตัวแปรที่ชิ้นส่วนซึ่ง extract ออกมาอ่าน แต่ไม่ได้เป็นเจ้าของ ควรจัดการอย่างไร?
Extract Function เป็นวิธีรักษาหลักของ smell แบบใด?
อะไรคือท่าที่กลับกันกับ Extract Function?