Split Loop
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”loop เดียวมักโดนยัดให้ทำสองอย่างพร้อมกัน เช่น รวมยอดของสิ่งหนึ่งไปพลางหาค่าสูงสุดของอีกสิ่ง หรือเก็บรายชื่อไปพลางนับจำนวนที่ตรงเงื่อนไข ให้ซอยออกเป็นสอง loop แต่ละตัววน collection เดิมแต่ทำงานเดียวเป๊ะ ๆ จากนั้นแต่ละ loop จะอ่านเป็นแนวคิดเดียว และคุณจะ refactor หรือย้ายทีละตัวได้อย่างอิสระ
อาการของปัญหา
หัวข้อที่มีชื่อว่า “อาการของปัญหา”คุณอ่าน loop หนึ่งแล้วต้องนั่งสางเส้นด้ายสองเส้นในหัว “ตรงนี้บวกยอดรวม… ส่วนตรงนี้ที่แทรกอยู่ด้วย คอยตามหาคนอายุน้อยที่สุด” ตัวสะสมสองตัวที่ไม่เกี่ยวกันเลยมาใช้ loop ร่วมกัน เพียงเพราะบังเอิญเดินบน list เดียวกัน ความผูกพันนั้นทำให้ loop ตั้งชื่อยาก extract ยาก และแก้ยาก เพราะแตะ logic ยอดรวมทีไรก็เสี่ยงกระทบ logic หาอายุน้อยสุดไปด้วย
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”loop ตัวนี้วนพนักงานเพื่อคำนวณเงินเดือนรวม และ หาอายุน้อยที่สุดไปด้วย ทั้งที่สองเรื่องไม่เกี่ยวกันเลย สองงานอยู่ใน loop เดียว เราจึงซอยออกจากกัน
// Before — one loop, two unrelated jobsfunction report(people: Person[]): { total: number; youngest: number } { let total = 0; let youngest = people[0]?.age ?? Infinity; for (const p of people) { total += p.salary; if (p.age < youngest) youngest = p.age; } return { total, youngest };}
// After — each loop has a single purposefunction report(people: Person[]): { total: number; youngest: number } { return { total: totalSalary(people), youngest: youngestAge(people) };}
function totalSalary(people: Person[]): number { let total = 0; for (const p of people) total += p.salary; return total;}
function youngestAge(people: Person[]): number { let youngest = people[0]?.age ?? Infinity; for (const p of people) { if (p.age < youngest) youngest = p.age; } return youngest;}# Before — one loop, two unrelated jobsdef report(people): total = 0 youngest = people[0].age if people else float("inf") for p in people: total += p.salary if p.age < youngest: youngest = p.age return {"total": total, "youngest": youngest}
# After — each loop has a single purposedef report(people): return {"total": total_salary(people), "youngest": youngest_age(people)}
def total_salary(people): total = 0 for p in people: total += p.salary return total
def youngest_age(people): youngest = people[0].age if people else float("inf") for p in people: if p.age < youngest: youngest = p.age return youngest// Before — one loop, two unrelated jobsfunc report(people []Person) (total, youngest int) { youngest = math.MaxInt for _, p := range people { total += p.Salary if p.Age < youngest { youngest = p.Age } } return total, youngest}
// After — each loop has a single purposefunc report(people []Person) (int, int) { return totalSalary(people), youngestAge(people)}
func totalSalary(people []Person) int { total := 0 for _, p := range people { total += p.Salary } return total}
func youngestAge(people []Person) int { youngest := math.MaxInt for _, p := range people { if p.Age < youngest { youngest = p.Age } } return youngest}// Before — one loop, two unrelated jobsfn report(people: &[Person]) -> (i64, i64) { let mut total = 0; let mut youngest = i64::MAX; for p in people { total += p.salary; if p.age < youngest { youngest = p.age; } } (total, youngest)}
// After — each loop has a single purposefn report(people: &[Person]) -> (i64, i64) { (total_salary(people), youngest_age(people))}
fn total_salary(people: &[Person]) -> i64 { people.iter().map(|p| p.salary).sum()}
fn youngest_age(people: &[Person]) -> i64 { people.iter().map(|p| p.age).min().unwrap_or(i64::MAX)}กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- Copy loop ทั้งหมดเพื่อให้คุณมีสอง loop ที่เหมือนกันวนคอลเล็กชันเดียวกัน
- ใน loop แรก ลบคำสั่งที่เป็นของงาน ที่สอง ใน loop ที่สอง ลบคำสั่งที่เป็นของงาน แรก ตอนนี้แต่ละ loop ทำงานเดียว
- รัน test behavior ต้องไม่เปลี่ยนแปลง
- ใช้ Extract Function กับแต่ละ loop ให้กลายเป็น query ที่ชื่อดี ๆ อย่าง
totalSalaryหรือyoungestAge - รัน test อีกครั้ง — ตอนนี้ caller อ่านเป็นผลลัพธ์ที่มีชื่อสองอย่างแทนที่จะเป็น loop เดียวที่พันกัน
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”ซอย loop ทุกครั้งที่ loop นั้นแบกความรับผิดชอบสองอย่างซึ่งคุณต้องใช้คำว่า “และ” มาอธิบาย ผลตอบแทนคือความชัดเจนและการเอากลับมาใช้ซ้ำ เพราะแต่ละ loop ตั้งชื่อง่าย extract ง่าย และแก้ได้อิสระ แถม Split Loop ยังมักเป็นก้าวแรกก่อนจะแทนแต่ละชิ้นด้วย pipeline operation ที่ชัดเจน
ข้อค้านที่เห็นชัดคือเรื่อง performance เพราะตอนนี้วนสองรอบ แต่ใน code จริงเกือบทั้งหมด ต้นทุนตรงนั้นแทบไม่มีนัยเมื่อเทียบกับความอ่านง่ายที่ได้มา และการยืนกรานวนรอบเดียวก็คือ premature optimisation ถ้าภายหลัง profile พิสูจน์ว่า loop ตัวนั้นร้อนจริง คุณค่อยรวมกลับก็ได้ แต่ให้ optimize จาก code ที่สะอาดและมีชื่อดี ไม่ใช่จากก้อนที่พันกันอยู่
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Split Loop เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| loop ทำสองอย่างที่ไม่เกี่ยวกัน — อ่านยาก | performance สำคัญมาก — แยก loop เพิ่ม iteration |
| ต้องการ Extract Function แต่ loop ทำหลาย accumulation | loop มีแค่หนึ่ง operation ที่ชัดเจน |
| ต้องการ test แต่ละส่วนแยกกัน | สองส่วนนั้นต้องใช้ result ของกันและกัน |
⚠️ ไม่ควร Split Loop เมื่อ:
- collection ใหญ่มากและ performance เป็น constraint หลัก
- สองส่วนใน loop นั้น tight-coupled กัน จนแยกไม่มีความหมาย
- เพิ่ม complexity โดยที่ readability ไม่ได้ดีขึ้นจริง