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

Split Loop

loop เดียวมักโดนยัดให้ทำสองอย่างพร้อมกัน เช่น รวมยอดของสิ่งหนึ่งไปพลางหาค่าสูงสุดของอีกสิ่ง หรือเก็บรายชื่อไปพลางนับจำนวนที่ตรงเงื่อนไข ให้ซอยออกเป็นสอง loop แต่ละตัววน collection เดิมแต่ทำงานเดียวเป๊ะ ๆ จากนั้นแต่ละ loop จะอ่านเป็นแนวคิดเดียว และคุณจะ refactor หรือย้ายทีละตัวได้อย่างอิสระ

คุณอ่าน loop หนึ่งแล้วต้องนั่งสางเส้นด้ายสองเส้นในหัว “ตรงนี้บวกยอดรวม… ส่วนตรงนี้ที่แทรกอยู่ด้วย คอยตามหาคนอายุน้อยที่สุด” ตัวสะสมสองตัวที่ไม่เกี่ยวกันเลยมาใช้ loop ร่วมกัน เพียงเพราะบังเอิญเดินบน list เดียวกัน ความผูกพันนั้นทำให้ loop ตั้งชื่อยาก extract ยาก และแก้ยาก เพราะแตะ logic ยอดรวมทีไรก็เสี่ยงกระทบ logic หาอายุน้อยสุดไปด้วย

loop ตัวนี้วนพนักงานเพื่อคำนวณเงินเดือนรวม และ หาอายุน้อยที่สุดไปด้วย ทั้งที่สองเรื่องไม่เกี่ยวกันเลย สองงานอยู่ใน loop เดียว เราจึงซอยออกจากกัน

// Before — one loop, two unrelated jobs
function 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 purpose
function 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;
}
  1. Copy loop ทั้งหมดเพื่อให้คุณมีสอง loop ที่เหมือนกันวนคอลเล็กชันเดียวกัน
  2. ใน loop แรก ลบคำสั่งที่เป็นของงาน ที่สอง ใน loop ที่สอง ลบคำสั่งที่เป็นของงาน แรก ตอนนี้แต่ละ loop ทำงานเดียว
  3. รัน test behavior ต้องไม่เปลี่ยนแปลง
  4. ใช้ Extract Function กับแต่ละ loop ให้กลายเป็น query ที่ชื่อดี ๆ อย่าง totalSalary หรือ youngestAge
  5. รัน 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 ทำหลาย accumulationloop มีแค่หนึ่ง operation ที่ชัดเจน
ต้องการ test แต่ละส่วนแยกกันสองส่วนนั้นต้องใช้ result ของกันและกัน

⚠️ ไม่ควร Split Loop เมื่อ:

  • collection ใหญ่มากและ performance เป็น constraint หลัก
  • สองส่วนใน loop นั้น tight-coupled กัน จนแยกไม่มีความหมาย
  • เพิ่ม complexity โดยที่ readability ไม่ได้ดีขึ้นจริง
Split Loop แก้ปัญหาอะไร?
ขั้นตอนเชิงกลไกแรกของ Split Loop คืออะไร?
ข้อโต้แย้งที่พบบ่อยต่อ Split Loop คืออะไร และคำตอบที่มักใช้คืออะไร?
Split Loop มักตามด้วยการ refactor ใดบนแต่ละ loop ที่ได้ออกมา?