Extract Function
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”ย้ายชิ้นส่วน code ที่เป็นเอกภาพเข้าไปใน function ใหม่ แล้วตั้งชื่อตาม จุดประสงค์ ที่ทำหน้าที่อยู่ จุดเดิมเหลือแค่การเรียก helper จึงอ่านได้เหมือนรายการสั้น ๆ ของขั้นตอนที่มีชื่อ แทนที่จะเป็นรายละเอียดพรืดเป็นพืด
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”นี่คือวิธีรักษา Long Function และรักษาบล็อก code ใด ๆ ที่ต้องมี comment มาอธิบายว่าทำอะไรอยู่ ลองนึกถึง comment ที่เขียนว่า “ตอนนี้คำนวณภาษี” ไว้เหนือ code สิบบรรทัด นั่นคือป้ายไฟกระพริบว่า code สิบบรรทัดนั้นอยากเป็น function ชื่อ taxFor พอชื่อ function แบกเจตนาไว้แล้ว comment ก็ไม่จำเป็นอีกต่อไป และจุดที่เรียกก็อ่านง่ายขึ้น
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”function ใบเสร็จตัวนี้คำนวณ subtotal คิดภาษี แล้วพิมพ์ออกมา ตอนแรกทำทุกอย่างแบบ inline หลังจาก refactor แต่ละงานกลายเป็น helper ที่มีชื่อของตัวเอง
// Beforefunction 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)}`);}
// Afterfunction 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)}`);}# Beforedef print_receipt(items): subtotal = 0 for item in items: subtotal += item["price"] * item["qty"] tax = subtotal * 0.07 print(f"Subtotal: {subtotal:.2f}") print(f"Tax: {tax:.2f}") print(f"Total: {subtotal + tax:.2f}")
# Afterdef print_receipt(items): sub = subtotal(items) tax = tax_for(sub) print_lines(sub, tax)
def subtotal(items): return sum(item["price"] * item["qty"] for item in items)
def tax_for(amount): return amount * 0.07
def print_lines(sub, tax): print(f"Subtotal: {sub:.2f}") print(f"Tax: {tax:.2f}") print(f"Total: {sub + tax:.2f}")// Beforefunc PrintReceipt(items []Item) { subtotal := 0.0 for _, item := range items { subtotal += item.Price * float64(item.Qty) } tax := subtotal * 0.07 fmt.Printf("Subtotal: %.2f\n", subtotal) fmt.Printf("Tax: %.2f\n", tax) fmt.Printf("Total: %.2f\n", subtotal+tax)}
// Afterfunc PrintReceipt(items []Item) { sub := subtotal(items) tax := taxFor(sub) printLines(sub, tax)}
func subtotal(items []Item) float64 { total := 0.0 for _, item := range items { total += item.Price * float64(item.Qty) } return total}
func taxFor(amount float64) float64 { return amount * 0.07}
func printLines(sub, tax float64) { fmt.Printf("Subtotal: %.2f\n", sub) fmt.Printf("Tax: %.2f\n", tax) fmt.Printf("Total: %.2f\n", sub+tax)}// Beforefn print_receipt(items: &[Item]) { let mut subtotal = 0.0; for item in items { subtotal += item.price * item.qty as f64; } let tax = subtotal * 0.07; println!("Subtotal: {:.2}", subtotal); println!("Tax: {:.2}", tax); println!("Total: {:.2}", subtotal + tax);}
// Afterfn print_receipt(items: &[Item]) { let sub = subtotal(items); let tax = tax_for(sub); print_lines(sub, tax);}
fn subtotal(items: &[Item]) -> f64 { items.iter().map(|item| item.price * item.qty as f64).sum()}
fn tax_for(amount: f64) -> f64 { amount * 0.07}
fn print_lines(sub: f64, tax: f64) { println!("Subtotal: {:.2}", sub); println!("Tax: {:.2}", tax); println!("Total: {:.2}", sub + tax);}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 กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- เลือกชิ้นส่วนที่จะ extract และตั้งชื่อที่บอกว่า ได้ผลลัพธ์อะไร ไม่ใช่บอกว่าทำอย่างไร ถ้าตั้งชื่อสะอาด ๆ ไม่ได้ แปลว่าชิ้นส่วนนั้นอาจยังไม่เป็นหน่วยเดียวกัน ลองขยับขอบเขตใหม่
- สร้าง function เปล่าที่ใช้ชื่อนั้น
- คัดลอกชิ้นส่วนเดิมเข้าไปใน function ใหม่
- ดูว่าชิ้นส่วนนั้นอ่านตัวแปรอะไรบ้าง แล้วส่งเข้าไปเป็น parameter ส่วนตัวแปรที่สร้างขึ้นใหม่และ code ข้างนอกยังต้องใช้ ก็คืนค่าออกมา
- แทนที่ชิ้นส่วนเดิมด้วยการเรียก function ใหม่
- รัน test behavior ต้องไม่เปลี่ยน
- ทำซ้ำกับชิ้นส่วนถัดไป 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 อ่านยากจริง ๆ