Fast Beats Right
เชื่อว่ารีบทำเสร็จดีกว่าทำให้ถูกเสมอ — จนหนี้เทคนิคท่วม
ปัญหาคืออะไร
หัวข้อที่มีชื่อว่า “ปัญหาคืออะไร”Fast Beats Right คือความเชื่อ (มักไม่ได้พูดออกมาตรง ๆ) ว่า การส่งมอบ feature ให้เสร็จไวกว่าย่อมดีกว่าการลงแรงทำให้ถูกต้องเสมอ ไม่ว่าจะเป็นแรงกดดันจากผู้บริหาร เดดไลน์ตลาด หรือวัฒนธรรมทีมที่วัดผลจากความเร็วอย่างเดียว ผลลัพธ์ที่ตามมาคือทีมเลือกทางลัดซ้ำแล้วซ้ำเล่า: ข้าม unit test เขียน code ซ้อน code เดิมแทนที่จะ refactor ปล่อยให้ layer ปนกัน หรือ hardcode ค่าที่ควรตั้งค่าได้
ตัวความเชื่อนี้ไม่ได้ผิดเสมอไป — บางครั้งการยอมรับ technical debt ระยะสั้นเพื่อพิสูจน์ตลาด (validate a hypothesis) หรือทันเดดไลน์สำคัญก็เป็นการตัดสินใจที่สมเหตุสมผล ปัญหาที่แท้จริงเกิดเมื่อ “รีบก่อน แก้ทีหลัง” กลายเป็น ข้ออ้างถาวร ไม่มีแผนชดใช้หนี้ ไม่มีใครกลับมาแก้ และรอบถัดไปก็ยังรีบต่ออีก จนหนี้เทคนิคสะสมพ้นจุดที่จะจัดการได้ทัน
Ward Cunningham ผู้บัญญัติศัพท์ technical debt เปรียบไว้ว่า การส่ง “code ที่ยังไม่ถูกต้องสมบูรณ์” ออกไปครั้งแรกก็เหมือนการก่อหนี้ — มันช่วยเร่งการพัฒนาได้ตราบใดที่รีบชดใช้คืนด้วยการ rewrite ในภายหลัง แต่ถ้าไม่ชดใช้ “ดอกเบี้ย” จะเกิดขึ้นทุกนาทีที่ต้องทำงานอยู่บน code ที่ยังไม่ถูกต้องนั้น
ทำไมถึงดูน่าใช้
หัวข้อที่มีชื่อว่า “ทำไมถึงดูน่าใช้”- ผลลัพธ์เห็นทันที feature ใช้งานได้ ผู้ใช้พอใจ ผู้บริหารพอใจ ในระยะสั้นดูเหมือนไม่มีต้นทุนแฝง
- เดดไลน์จริง บางครั้งการ validate สมมติฐานทางธุรกิจให้ทันงานเปิดตัว หรือปิดดีลลูกค้าสำคัญ มีมูลค่าสูงกว่าความสะอาดของ code จริง ๆ — นี่คือสิ่งที่ Martin Fowler เรียกว่าหนี้แบบ “deliberate and prudent” คือรู้ตัวและตัดสินใจอย่างมีเหตุผล
- การวัดผลงานแบบ velocity เท่านั้น เมื่อทีมถูกประเมินจากจำนวน feature ที่ส่งมอบต่อ sprint ไม่ใช่จาก maintainability ระยะยาว การรีบย่อมดูเป็นกลยุทธ์ที่ “ชนะ” เสมอ
- ต้นทุนของ debt มองไม่เห็นในบัญชี ต่างจากหนี้การเงินที่มีตัวเลขดอกเบี้ยชัดเจน หนี้เทคนิคไม่มีใบแจ้งหนี้ ไม่มีใครเห็นความช้าที่สะสมจนกว่าจะสายเกินไป
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”เมื่อ “รีบก่อน” กลายเป็นค่าเริ่มต้นถาวรแทนที่จะเป็นการตัดสินใจครั้งคราว ผลกระทบจะทบต้นอย่างรวดเร็ว:
- ดอกเบี้ยกินเร็วกว่าที่คิด Fowler ชี้ว่าทีมที่พยายามเร่งความเร็วด้วยการสะสม cruft มักจะข้าม “design payoff line” — จุดที่ต้นทุนของหนี้เริ่มมากกว่าประโยชน์ที่ได้จากความเร็ว — ภายในไม่กี่สัปดาห์ ไม่ใช่ไม่กี่เดือน สุดท้ายทีมเหล่านี้กลับส่งมอบ “ช้ากว่า” ทีมที่ลงทุนกับคุณภาพภายในตั้งแต่แรก
- code ยิ่งเปราะ ยิ่งแตะยาก การข้ามทดสอบ การก็อปวางแทน refactor ทำให้ codebase เปราะบางลงเรื่อย ๆ ทุกการเปลี่ยนแปลงเล็ก ๆ มีความเสี่ยงสูงขึ้น
- ไม่มีจุดหยุด ถ้าไม่มีการวางแผนชดใช้หนี้อย่างจริงจัง (deliberate repayment) ทีมจะรีบต่อไปเรื่อย ๆ เพราะทุกรอบก็มีเดดไลน์ใหม่เสมอ — หนี้จึงไม่เคยถูกชำระ
- Productivity ตกโดยไม่มีใครรู้ตัว เพราะไม่มีหน่วยวัดที่จับต้องได้ ทีมและผู้บริหารมักไม่รู้ว่ากำลังช้าลงจนกว่าจะสายเกินแก้ — deadline พลาดซ้ำ ๆ, bug เพิ่มขึ้น, พนักงานลาออกเพราะเบื่อหน่ายกับการดับไฟตลอดเวลา
- สุดโต่งฝั่งตรงข้ามก็อันตราย การหมกมุ่นกับความถูกต้องจนไม่ยอมส่งมอบเลยก็เป็นปัญหาเช่นกัน — นั่นคือ Analysis Paralysis ความสมดุลที่แท้จริงอยู่ตรงกลาง ไม่ใช่สุดขั้วใดขั้วหนึ่ง
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ตัวอย่างต่อไปนี้แสดงการ “รีบทำให้เสร็จ” แบบ Fast Beats Right — เขียน business logic ปนกับ data access ใน method เดียว ไม่มี validation ไม่มี error handling ที่ควรมี เพราะ “ไม่มีเวลา”:
// รีบทำให้เสร็จ: ทุกอย่างยัดไว้ใน controller เดียว ไม่มี validation ไม่มี testpublic class OrderController{ private readonly SqlConnection _connection;
public OrderController(string connectionString) { _connection = new SqlConnection(connectionString); }
public void PlaceOrder(int customerId, int productId, int quantity) { // ไม่ตรวจสอบ quantity ติดลบหรือ customerId ไม่มีอยู่จริง // เพราะ "เดี๋ยวค่อยกลับมาทำ" — แต่ไม่มีใครกลับมาทำจริง _connection.Open(); var cmd = new SqlCommand( $"INSERT INTO Orders (CustomerId, ProductId, Quantity) VALUES ({customerId}, {productId}, {quantity})", _connection); // SQL injection: อีกหนี้ที่แอบซ่อนอยู่ cmd.ExecuteNonQuery();
// ลด stock แบบไม่ใช้ transaction ถ้า insert สำเร็จแต่ update พลาด สต๊อกจะเพี้ยน var updateCmd = new SqlCommand( $"UPDATE Products SET Stock = Stock - {quantity} WHERE Id = {productId}", _connection); updateCmd.ExecuteNonQuery(); _connection.Close(); }}code นี้ “ทำงานได้” ในเดโมและปิดสปรินต์ได้ตรงเวลา แต่ทุกจุดคือหนี้ที่รอเก็บดอกเบี้ย: ไม่มี validation, ไม่มี transaction, เสี่ยง SQL injection, ผูก data access กับ controller จน test แยกไม่ได้
ทางแก้ไม่จำเป็นต้องหมายถึง “ช้าลงเสมอ” — แค่แยกความรับผิดชอบและปิดช่องโหว่ที่ชัดเจนที่สุดก่อน แล้วค่อยขยายทีหลังตามความจำเป็น:
// แก้แล้ว: แยก validation, ใช้ transaction, ผ่าน repository ที่ test ได้public class OrderService{ private readonly IOrderRepository _orders; private readonly IProductRepository _products;
public OrderService(IOrderRepository orders, IProductRepository products) { _orders = orders; _products = products; }
public Result PlaceOrder(int customerId, int productId, int quantity) { if (quantity <= 0) { return Result.Fail("Quantity must be positive."); }
var product = _products.Find(productId); if (product is null || product.Stock < quantity) { return Result.Fail("Product unavailable in requested quantity."); }
// ทำ insert order และ decrement stock ใน transaction เดียว using var transaction = _orders.BeginTransaction(); _orders.Add(new Order(customerId, productId, quantity)); product.DecrementStock(quantity); _products.Update(product); transaction.Commit();
return Result.Ok(); }}code ที่แก้แล้วไม่ได้ “สมบูรณ์แบบ” หรือช้ากว่าอย่างมีนัยสำคัญ — มันแค่ปิดความเสี่ยงที่ชัดเจนที่สุดโดยใช้เวลาเพิ่มไม่มาก นี่คือจุดสมดุลที่ Fast Beats Right มักมองข้าม
ทางแก้และการ refactor
หัวข้อที่มีชื่อว่า “ทางแก้และการ refactor”- ตัดสินใจก่อหนี้อย่างมีสติ (deliberate, not reckless) ใช้กรอบ Technical Debt Quadrant ของ Fowler: หนี้แบบ “deliberate & prudent” (รู้ตัวและมีเหตุผล) ต่างจาก “reckless” (มักง่าย ไม่คิดผลกระทบ) ถ้าจะรีบ ให้รีบแบบรู้ตัวว่ากำลังก่อหนี้อะไร และวางแผนจะจ่ายคืนอย่างไร
- บันทึกหนี้ให้เห็นชัด ติด
// TODOหรือเปิด ticket ทันทีที่ตัดสินใจข้ามบางอย่างไปก่อน อย่าปล่อยให้หนี้ซ่อนอยู่ในหัวคนเดียว - จัดสรรเวลาเพื่อชดใช้หนี้เป็นประจำ เช่น กันสัดส่วนงานต่อ sprint ไว้สำหรับ refactor หนี้เก่า แทนที่จะรอให้ “มีเวลาว่าง” ซึ่งไม่มาถึงจริง ๆ
- ทำเรื่องเล็กให้ถูกต้องได้เร็วเหมือนกัน validation พื้นฐาน, transaction, การแยก layer มักไม่ได้ใช้เวลาเพิ่มมากอย่างที่คิด — สิ่งที่ทำให้ “ถูกต้อง” ช้าคือการขาดวินัยและ pattern ที่เคยชิน ไม่ใช่ตัวงานเอง
- ใช้ Tolerance for Imperfection แทนสองสุดขั้ว ยอมรับว่า code ไม่ต้องสมบูรณ์แบบตั้งแต่แรก แต่ก็ไม่ปล่อยให้ “ไม่สมบูรณ์” กลายเป็นค่าเริ่มต้นถาวร — เดินสายกลางระหว่าง Fast Beats Right กับ Analysis Paralysis
- ระวังสัญญาณเริ่มต้นของ Duct Tape Coder และ Broken Windows — ถ้าเริ่มเห็นทีมชื่นชม “คนแก้ไฟเร็ว” มากกว่า “คนป้องกันไฟ” นั่นคือสัญญาณว่าวัฒนธรรมกำลังเอียงไปทาง Fast Beats Right แบบ reckless
flowchart TD
Deadline[แรงกดดันเดดไลน์] --> Choice{ตัดสินใจอย่างมีสติหรือไม่}
Choice -->|มีสติ วางแผนคืนหนี้| Prudent[Deliberate and Prudent debt]
Choice -->|ไม่คิดผลกระทบ| Reckless[Deliberate and Reckless debt]
Prudent --> Repay[จัดสรรเวลา refactor สม่ำเสมอ]
Reckless --> Pileup[หนี้สะสมทบต้น]
Repay --> Healthy[ความเร็วยั่งยืน]
Pileup --> Slowdown[ทีมช้าลงกว่าตอนไม่รีบ]
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Tolerance for Imperfection
- Analysis Paralysis
- Duct Tape Coder
- Broken Windows
- Death March
- Boy Scout Rule