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

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 ไม่มี test
public 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 มัก​มอง​ข้าม

  1. ตัดสิน​ใจ​ก่อ​หนี้​อย่าง​มี​สติ (deliberate, not reckless) ใช้​กรอบ Technical Debt Quadrant ของ Fowler: หนี้​แบบ “deliberate & prudent” (รู้ตัว​และ​มี​เหตุผล) ต่าง​จาก “reckless” (มักง่าย ไม่​คิด​ผลกระทบ) ถ้า​จะ​รีบ ให้​รีบ​แบบ​รู้ตัว​ว่า​กำลัง​ก่อ​หนี้​อะไร และ​วางแผน​จะ​จ่าย​คืน​อย่างไร
  2. บันทึก​หนี้​ให้​เห็น​ชัด ติด // TODO หรือ​เปิด ticket ทันที​ที่​ตัดสิน​ใจ​ข้าม​บาง​อย่าง​ไป​ก่อน อย่า​ปล่อย​ให้​หนี้​ซ่อน​อยู่​ใน​หัว​คน​เดียว
  3. จัดสรร​เวลา​เพื่อ​ชดใช้​หนี้​เป็น​ประจำ เช่น กัน​สัดส่วน​งาน​ต่อ sprint ไว้​สำหรับ refactor หนี้​เก่า แทนที่​จะ​รอ​ให้ “มี​เวลา​ว่าง” ซึ่ง​ไม่​มา​ถึง​จริง ๆ
  4. ทำ​เรื่อง​เล็ก​ให้​ถูกต้อง​ได้​เร็ว​เหมือน​กัน validation พื้นฐาน, transaction, การ​แยก layer มัก​ไม่​ได้​ใช้​เวลา​เพิ่ม​มาก​อย่าง​ที่​คิด — สิ่ง​ที่​ทำให้ “ถูกต้อง” ช้า​คือ​การ​ขาด​วินัย​และ pattern ที่​เคยชิน ไม่ใช่​ตัว​งาน​เอง
  5. ใช้ Tolerance for Imperfection แทน​สอง​สุด​ขั้ว ยอมรับ​ว่า code ไม่​ต้อง​สมบูรณ์​แบบ​ตั้งแต่​แรก แต่​ก็​ไม่​ปล่อย​ให้ “ไม่​สมบูรณ์” กลาย​เป็น​ค่า​เริ่มต้น​ถาวร — เดินสาย​กลาง​ระหว่าง Fast Beats Right กับ Analysis Paralysis
  6. ระวัง​สัญญาณ​เริ่มต้น​ของ 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[ทีมช้าลงกว่าตอนไม่รีบ]