Bump Road
code ที่ทำงานด้วยลำบาก เพราะแรงเสียดทานที่สะสมทีละนิด
กลิ่นนี้คืออะไร
หัวข้อที่มีชื่อว่า “กลิ่นนี้คืออะไร”Bump Road อธิบาย code ที่ ทำงานด้วยยากในทางปฏิบัติ — ไม่ใช่เพราะข้อบกพร่องเดียวที่ชัดเจน แต่เพราะแรงเสียดทานเล็ก ๆ หลายจุดสะสมรวมกันจนทุกก้าวยากกว่าที่ควรจะเป็น เหมือนขับรถบนถนนเต็มไปด้วยหลุมบ่อ — ไปถึงปลายทางได้ แต่ไม่สบาย ช้า และเสี่ยงพลาดง่ายกว่าถนนเรียบ
DevIQ ใช้ภาพนี้ในความหมายกว้าง คือความไม่ราบรื่นสะสมใน codebase ส่วนคำเดียวกันในวงการวิเคราะห์ code (Adam Tornhill / CodeScene) ก็ใช้ชื่อ “Bumpy Road” อธิบายอาการเดียวกันในระดับ 1 function เดียว: function ที่มีเงื่อนไขซ้อนลึก (nested conditional) หลายก้อนเรียงต่อกัน — เวลามองรูปทรงการเยื้อง (indentation) ของ code จะเห็นเป็นลอนสูงต่ำสลับกันเหมือนถนนเป็นหลุมเป็นบ่อจริง ๆ แต่ละ “หลุม” คือก้อนตรรกะที่ควรจะถูกห่อ (encapsulate) เป็นของตัวเอง แต่กลับถูกปล่อยให้นอนแช่อยู่ใน function เดียวกัน
แต่ละหลุมอาจเล็ก แต่รวมกันเก็บภาษีมหาศาลจากนักพัฒนาทุกคนที่ต้องอ่านหรือแก้ code นั้น ทางแก้ที่ DevIQ เสนอคือ Boy Scout Rule — ทำถนนให้เรียบทีละนิดในทุก commit (เทียบกับปรากฏการณ์ Broken Windows ที่ความเสื่อมโทรมเล็ก ๆ ที่ไม่มีใครแก้ จะชวนให้เกิดความเสื่อมโทรมเพิ่มขึ้นเรื่อย ๆ)
วิธีสังเกต
หัวข้อที่มีชื่อว่า “วิธีสังเกต”สัญญาณที่บอกว่ากำลังเจอ Bump Road:
- เปิด function ขึ้นมาแล้วเห็น indentation เป็นลอน — ส่วนหัวราบ ตามด้วย block
if/forที่ซ้อนลึก 3-4 ชั้น แล้วกลับมาราบ แล้วซ้อนลึกอีกรอบ ทำซ้ำหลายครั้งใน function เดียว - นับ “จำนวนหลุม” ได้มากกว่า 1-2 หลุมใน function เดียว — แต่ละหลุมมักแทนความรับผิดชอบ (responsibility) คนละเรื่องกัน แต่ไม่เคยถูกแยกออกมา
- อ่าน code แล้วต้อง “ถือ” เงื่อนไขหลายชั้นไว้ในหัวพร้อมกันเพื่อจะเข้าใจว่าบรรทัดปัจจุบันทำงานภายใต้เงื่อนไขอะไรบ้าง
- แก้ bug เล็ก ๆ หนึ่งจุด แต่ต้องไล่สายตาผ่านหลุมอื่นที่ไม่เกี่ยวข้องเพื่อยืนยันว่าไม่กระทบกัน
- ทีมเริ่มหลีกเลี่ยง file/function นี้ พูดแบบไม่เป็นทางการว่า “อย่าไปแตะตรงนั้น” — เป็นสัญญาณว่าถนนเป็นหลุมจนไม่มีใครอยากขับผ่าน
เครื่องมืออย่าง CodeScene วัดกลิ่นนี้ได้อัตโนมัติจากสามปัจจัย: ความลึกของการซ้อน (nesting depth) จำนวนหลุม (number of bumps) และขนาดของแต่ละหลุม (bump size) แล้วรวมเข้ากับดัชนี Code Health ของทั้ง file
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”หน่วยความจำทำงาน (working memory) ของสมองมนุษย์มีขีดจำกัด งานวิจัยที่ CodeScene อ้างอิงชี้ว่าอาจถือข้อมูลพร้อมกันได้เพียง 3-4 ชิ้นเท่านั้น เงื่อนไขซ้อนลึกหลายก้อนบังคับให้ผู้อ่านต้อง “แบก” บริบทของทุกชั้นไว้ในหัวพร้อมกัน — ยิ่งซ้อนลึกและมีหลายหลุม ภาระทางปัญญา (cognitive load) ก็ยิ่งสูง และงานวิจัยพบว่าความซับซ้อนจากการซ้อนมีความสัมพันธ์สูงกับอัตราการเกิด bug
ในภาษาเชิงคำสั่ง (imperative language) ยังมีความเสี่ยงเพิ่มคือ feature entanglement — ตัวแปรสถานะที่ใช้ร่วมกันข้ามหลายหลุมทำให้การจัดการสถานะ (state management) ซับซ้อนขึ้นไปอีก แก้จุดหนึ่งอาจกระทบอีกจุดที่ดูเหมือนไม่เกี่ยวข้องกันเลย
ในมุมที่กว้างกว่าระดับ function เดียว — ความหมายดั้งเดิมของ DevIQ — แรงเสียดทานสะสมยังมีต้นทุนเชิงองค์กรด้วย: นักพัฒนาใหม่ใช้เวลานานขึ้นกว่าจะ “ชิน” กับถนน เวลาประเมินงานคลาดเคลื่อนเพราะไม่มีใครรู้ว่าจะเจอหลุมกี่หลุมระหว่างทาง และทีมเริ่มเลี่ยงพื้นที่นั้นแทนที่จะซ่อมมัน — เป็นวงจรเดียวกับ Broken Windows
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”ตัวอย่าง function ประมวลผลออเดอร์ที่มีสามหลุม: ตรวจการชำระเงิน ตรวจสต็อก และคำนวณค่าส่ง — แต่ละหลุมซ้อนเงื่อนไข 3 ชั้นขึ้นไป และแทรกอยู่ใน function เดียวกันหมด
// ก่อน refactor: 1 function สามหลุม อ่านยาก แก้เสี่ยงpublic OrderResult ProcessOrder(Order order, Customer customer, Warehouse warehouse){ var result = new OrderResult();
// หลุมที่ 1: ตรวจการชำระเงิน if (order.PaymentMethod != null) { if (order.PaymentMethod.IsValid) { if (order.Total <= customer.CreditLimit) { result.PaymentApproved = true; } else { result.Errors.Add("เกินวงเงินเครดิต"); } } else { result.Errors.Add("วิธีชำระเงินไม่ถูกต้อง"); } } else { result.Errors.Add("ไม่พบวิธีชำระเงิน"); }
// หลุมที่ 2: ตรวจสต็อกสินค้า if (result.PaymentApproved) { foreach (var item in order.Items) { if (warehouse.HasStock(item.Sku)) { if (warehouse.GetStock(item.Sku) >= item.Quantity) { result.ReservedItems.Add(item); } else { result.Errors.Add($"สต็อกไม่พอสำหรับ {item.Sku}"); } } else { result.Errors.Add($"ไม่พบสินค้า {item.Sku} ในคลัง"); } } }
// หลุมที่ 3: คำนวณค่าส่ง if (result.Errors.Count == 0) { if (customer.Address.Country == "TH") { if (order.Total > 1000) { result.ShippingFee = 0; } else { result.ShippingFee = 50; } } else { result.ShippingFee = 500; } }
return result;}ทั้งสามหลุมเป็นคนละความรับผิดชอบกันโดยสิ้นเชิง — การอ่าน function นี้ต้องไล่เยื้องขึ้นลงสามรอบ และตัวแปร result กลายเป็นจุดเชื่อมสถานะข้ามหลุมที่ทำให้แก้จุดหนึ่งเสี่ยงกระทบอีกจุด
refactor ด้วย Extract Method แยกแต่ละหลุมออกเป็น function ของตัวเอง และใช้ Replace Nested Conditional with Guard Clauses ทำให้แต่ละ function ย่อยแบนราบขึ้น:
// หลัง refactor: function หลักอ่านเป็นลำดับขั้นตอนตรงไปตรงมาpublic OrderResult ProcessOrder(Order order, Customer customer, Warehouse warehouse){ var result = new OrderResult();
ApprovePayment(order, customer, result); if (!result.PaymentApproved) return result;
ReserveStock(order, warehouse, result); if (result.Errors.Count > 0) return result;
result.ShippingFee = CalculateShippingFee(order, customer); return result;}
private void ApprovePayment(Order order, Customer customer, OrderResult result){ if (order.PaymentMethod is null) { result.Errors.Add("ไม่พบวิธีชำระเงิน"); return; } if (!order.PaymentMethod.IsValid) { result.Errors.Add("วิธีชำระเงินไม่ถูกต้อง"); return; } if (order.Total > customer.CreditLimit) { result.Errors.Add("เกินวงเงินเครดิต"); return; } result.PaymentApproved = true;}
private void ReserveStock(Order order, Warehouse warehouse, OrderResult result){ foreach (var item in order.Items) { if (!warehouse.HasStock(item.Sku)) { result.Errors.Add($"ไม่พบสินค้า {item.Sku} ในคลัง"); continue; } if (warehouse.GetStock(item.Sku) < item.Quantity) { result.Errors.Add($"สต็อกไม่พอสำหรับ {item.Sku}"); continue; } result.ReservedItems.Add(item); }}
private decimal CalculateShippingFee(Order order, Customer customer){ if (customer.Address.Country != "TH") return 500; return order.Total > 1000 ? 0 : 50;}ผลลัพธ์: function หลักอ่านได้เหมือนรายการขั้นตอน (ตรวจเงิน → จองสต็อก → คิดค่าส่ง) แต่ละ function ย่อยมีความรับผิดชอบเดียว ทดสอบแยกหน่วยได้เอง และไม่มีจุดไหนต้องซ้อนเกิน 2 ชั้น — ถนนเรียบขึ้นทั้งเส้น
graph TD
A[ProcessOrder เดิม] --> B[หลุม 1 ตรวจเงิน]
A --> C[หลุม 2 ตรวจสต็อก]
A --> D[หลุม 3 คิดค่าส่ง]
B --> E[ApprovePayment]
C --> F[ReserveStock]
D --> G[CalculateShippingFee]
หลักการเลือกว่าจะแยกกี่ function และตรงไหน: แต่ละหลุมที่มีชื่อเรียกได้ใน domain (ตรวจเงิน, จองสต็อก, คิดค่าส่ง) คือผู้สมัครที่ดีสำหรับ Extract Method — ถ้าไม่รู้จะตั้งชื่ออะไร มักแปลว่าหลุมนั้นยังไม่ใช่ความรับผิดชอบเดี่ยว ๆ จริง ต้องแยกเงื่อนไขให้ชัดก่อนด้วย Decompose Conditional
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Boy Scout Rule — ทำถนนให้เรียบขึ้นทีละนิดทุกครั้งที่ผ่าน แทนที่จะปล่อยหลุมไว้
- Broken Windows — ความเสื่อมโทรมเล็ก ๆ ที่ไม่มีใครแก้ ชวนให้เกิดความเสื่อมโทรมเพิ่ม
- Code Readability — เป้าหมายปลายทางของการปรับถนนให้เรียบ
- Conditional Complexity — กลิ่นพี่น้องที่อธิบายตรรกะเงื่อนไขซับซ้อนในระดับเดียวกัน
- Long Method — Bump Road มักโผล่คู่กับ function ที่ยาวเกินไป
- Mixed Levels of Abstraction — แต่ละหลุมมักผสมระดับ abstraction ที่ต่างกันไว้ในที่เดียว
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/code-smells/bump-road
- The Bumpy Road Code Smell: Measuring Code Complexity by its Shape and Distribution — CodeScene
- Bumpy Road — Samman Coaching
- Decompose Conditional — Refactoring.Guru
- Replace Nested Conditional with Guard Clauses — Refactoring.Guru
- Extract Method — Refactoring.Guru