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

Broken Windows

ปัญหา​เล็ก​ที่​ปล่อย​ไว้ ส่ง​สัญญาณ​ว่า​ไม่มี​ใคร​ใส่ใจ แล้ว​ปัญหา​ก็​ทวีคูณ

Broken Windows เป็น​แอนตี้ pattern ที่​ยืม​ชื่อ​มา​จาก​ทฤษฎี​อาชญา​วิทยา​ของ James Q. Wilson และ George L. Kelling ที่​ตี​พิมพ์​ใน​นิตยสาร The Atlantic Monthly ปี 1982 ทฤษฎี​นี้​อธิบาย​ว่า​อาคาร​ที่​มี​กระจก​แตก​เพียง​บาน​เดียว​แล้ว​ไม่มี​ใคร​ซ่อม จะ​ส่ง​สัญญาณ​ว่า “ไม่มี​ใคร​ดูแล​ที่​นี่” และ​ดึงดูด​ความ​เสื่อมโทรม​เพิ่ม​ขึ้น​เรื่อย ๆ — กระจก​แตก​อีก​บาน ขยะ​กอง graph ฟิตี้ จน​สุดท้าย​กลาย​เป็น​แหล่ง​อาชญากรรม​ร้ายแรง งาน​วิจัย​ที่​ทฤษฎี​นี้​อ้างอิง​มา​จาก​การ​ทดลอง​ของ​นัก​จิตวิทยา Philip Zimbardo แห่ง Stanford เรื่อง​รถ​ที่​ถูก​ทิ้ง​ไว้​กลาง​ถนน

Andy Hunt และ Dave Thomas นำ​แนวคิด​นี้​มา​ปรับ​ใช้​กับ​ซอฟต์แวร์​เป็น​ครั้ง​แรก​ใน The Pragmatic Programmer (1999) โดย​เสนอ​ว่า code ก็มี “หน้าต่าง​แตก” ของ​มัน​เอง ได้แก่ การ​ออกแบบ​ที่​แย่ ชื่อ​ตัวแปร​ที่​สื่อ​ความหมาย​ผิด คอมเมนต์ TODO ที่​ค้าง​มา​เป็น​ปี ๆ, catch block ที่​กลืน exception เงียบ ๆ, code ที่​ถูก​คอมเมนต์​ทิ้ง​ไว้​แทนที่​จะ​ลบ หรือ build ที่​ผ่าน​ทั้ง​ที่​มี warning เพียบ เมื่อ​สิ่ง​เหล่า​นี้​ถูก​ปล่อย​ไว้​โดย​ไม่มี​ใคร​แก้ ทีม​ทั้ง​ทีม​จะ​ค่อย ๆ ตีความ​ว่า “มาตรฐาน​คุณภาพ​ของ project นี้​คือ​แบบ​นี้​แหละ” และ​เริ่ม​เขียน code แบบ​เดียวกัน​ซ้ำ ๆ จน codebase ทั้ง​ก้อน​เสื่อม​สภาพ — ปรากฏการณ์​ที่​เรียก​กัน​ว่า code rot หรือ software entropy

  • “งาน​เข้า​เยอะ เดี๋ยว​ค่อย​แก้” — ภาย​ใต้ deadline การ​มอง​ข้าม code ที่​ไม่​สะอาด​ดู​เป็น​ทาง​เลือก​ที่​สม​เหตุ​สม​ผลกว่าการ​หยุด​แก้​ทันที
  • “มัน​ไม่ใช่ code ของ​ฉัน” — เมื่อ​ไม่มี code ownership ร่วม​กัน แต่ละ​คน​รู้สึก​ว่าการ​ซ่อม​หน้าต่าง​แตก​ของ​คน​อื่น​ไม่ใช่​หน้าที่​ตน
  • “ใน​เมื่อ​มัน​แย่​อยู่​แล้ว จะ​ดี​ขึ้น​อีก​นิด​ก็​ไม่​ต่าง​กัน” — เมื่อ​เห็น code แย่​อยู่​รอบ​ตัว​อยู่​แล้ว การ​เพิ่ม code แย่​อีก​ชิ้น​ดู​ไม่​สร้าง​ความ​เสียหาย​เพิ่ม​อย่าง​มี​นัย​สำคัญ​ใน​สายตา​คน​เขียน
  • ผลกระทบ​มอง​ไม่​เห็น​ทันที — หน้าต่าง​แตก​บาน​เดียว​ไม่​ได้​ทำให้​ระบบ​พัง​ทันที ผลลัพธ์​จึง​สะสม​อย่าง​เงียบ ๆ จนถึง​จุด​ที่​สังเกต​ได้​ก็​สาย​เกิน​จะ​แก้​ง่าย ๆ แล้ว

ปัญหา​ไม่​ได้​อยู่​ที่​หน้าต่าง​แตก​บาน​แรก​บาน​เดียว แต่​อยู่​ที่ สัญญาณ ที่​มัน​ส่ง​ออก​ไป เมื่อ​ทีม​เห็น​ว่า​ไม่มี​ใคร​แก้​ปัญหา​เล็ก ๆ พฤติกรรม​จะ​เลื่อน​ไถล​ตาม (ปรากฏการณ์​ที่​นัก​สังคมวิทยา​เรียก​ว่า normalization of deviance):

  • มาตรฐาน​เสื่อมถอย​เป็น​วงจร — code แย่​ดึงดูด code แย่​เพิ่ม เพราะ​นัก​พัฒนา​ใหม่ copy pattern ที่​เห็น​อยู่​แล้ว​ใน codebase แม้ pattern นั้น​จะ​แย่​ก็ตาม
  • ต้นทุน​แก้ไข​ทวีคูณ​ตาม​เวลา — หน้าต่าง​แตก​บาน​เดียว​ซ่อม​ง่าย​และ​ถูก แต่​ถ้า​ปล่อย​จน​กลาย​เป็น​ตึก​ทรุด​ทั้ง​หลัง การ​ซ่อม​จะ​แพง​กว่า​เดิม​หลาย​เท่า เหมือน technical debt ที่​ดอกเบี้ย​พอกพูน
  • บั่นทอน morale และ​วินัย​ของ​ทีม — เมื่อ passed-down convention กลาย​เป็น “ทาง​ลัด​คือ​มาตรฐาน” คน​ที่​อยาก​รักษา​คุณภาพ​จะ​รู้สึก​โดดเดี่ยว​หรือ​ถูก​มอง​ว่า​จู้จี้​เกิน​จำเป็น
  • ซ่อน bug และ​เพิ่ม​ความ​เสี่ยงcatch ที่​กลืน exception, code ตาย​ที่​ไม่มี​ใคร​กล้า​ลบ, หรือ test ที่​ถูก skip ไว้ ล้วน​ซ่อน​ปัญหา​จริง​ไว้​จนกว่า​จะ​ระเบิด​ใน​โปร​ดัก​ชัน
  • onboarding ยาก​ขึ้น — codebase ที่​เต็ม​ไป​ด้วย​ความ​ไม่​สอดคล้อง​กัน​ทำให้​นัก​พัฒนา​ใหม่​แยก​ไม่​ออกว่า​อะไร​คือ convention จริง อะไร​คือ​หน้าต่าง​แตก​ที่​ควร​เลี่ยง
flowchart TD
    A[หน้าต่างแตกบานแรก] --> B[ไม่มีใครซ่อม]
    B --> C[ทีมตีความว่าไม่มีใครสนใจคุณภาพ]
    C --> D[เกิดทางลัดเพิ่มขึ้นเรื่อยๆ]
    D --> E[Code Rot สะสม]
    E --> F[ความเร็วพัฒนาลดลง]
    F --> G[ตนทุนแก้ไขสูงขึ้นอีก]
    G --> B

code ต่อ​ไป​นี้​เริ่ม​จาก “หน้าต่าง​แตก” เล็ก ๆ หลาย​บาน — ชื่อ field ผิด​ขนบ, magic number, catch ที่​กลืน exception, และ code ตาย​ที่​ถูก​คอมเมนต์​ทิ้ง​ไว้:

// method เดียวที่ดูไม่มีพิษภัย แต่มีหน้าต่างแตกซ่อนอยู่หลายบาน
public class OrderProcessor
{
public void Process(Order o)
{
// TODO: validate ให้ครบทีหลัง ตอนนี้รีบส่ง deadline
if (o.Total > 1000)
{
o.discount_applied = true; // ชื่อ field ผิดขนบ ที่เหลือใช้ PascalCase กันหมด
}
try
{
_repository.Save(o);
}
catch (Exception)
{
// เดี๋ยวค่อยจัดการ ตอนนี้ไม่มีเวลา
}
// legacyCalculateTax(o); ปิดไว้ก่อน ยังไม่กล้าลบ เผื่อใช้
}
}

นัก​พัฒนา​คน​ถัด​ไป​ที่​แก้ file นี้​จะ​เห็น pattern เหล่า​นี้​เป็น “มาตรฐาน​ของ project” แล้ว​เลียน​แบบ​ต่อ — เพิ่ม catch (Exception) เปล่า ๆ อีก​จุด เพิ่ม field snake_case อีก​ตัว codebase ก็​เสื่อม​เร็ว​ขึ้น​เรื่อย ๆ

refactor ด้วย​การ​ซ่อม​หน้าต่าง​แตก​ทุก​บาน​ที่​เจอ (ตาม Boy Scout Rule) แทนที่​จะ​เดิน​ผ่าน​ไป​เฉย ๆ:

public class OrderProcessor
{
private const decimal BulkDiscountThreshold = 1000m;
private readonly ILogger<OrderProcessor> _logger;
private readonly IOrderRepository _repository;
public OrderProcessor(IOrderRepository repository, ILogger<OrderProcessor> logger)
{
_repository = repository;
_logger = logger;
}
public void Process(Order order)
{
ValidateOrder(order);
if (order.Total > BulkDiscountThreshold)
{
order.DiscountApplied = true;
}
try
{
_repository.Save(order);
}
catch (RepositoryException ex)
{
// บันทึก error จริง แทนที่จะกลืนเงียบ ๆ แล้วโยนต่อให้ผู้เรียกตัดสินใจ
_logger.LogError(ex, "บันทึกคำสั่งซื้อ {OrderId} ไม่สำเร็จ", order.Id);
throw;
}
}
private static void ValidateOrder(Order order)
{
if (order.Total < 0)
{
throw new ArgumentException("ยอดรวมคำสั่งซื้อต้องไม่ติดลบ", nameof(order));
}
}
}

การ​แก้ไข ได้แก่ (1) เปลี่ยน discount_applied เป็น DiscountApplied ให้​ตรง​ขนบ (2) ตั้ง​ชื่อ magic number 1000 เป็น BulkDiscountThreshold (3) จับ exception เฉพาะ​เจาะจง​และ log ก่อน​โยน​ต่อ แทน​การ​กลืน​เงียบ ๆ (4) ลบ code ตาย​ที่​คอมเมนต์​ทิ้ง​ไว้​ทั้งหมด — ถ้า​จำเป็น​ต้อง​ใช้​จริง​ใน​อนาคต ระบบ version control เก็บ​ประวัติ​ไว้​ให้​อยู่​แล้ว ไม่​ต้อง​แช่ code ตาย​ไว้​ใน file

  • Boy Scout Rule — ทุก​ครั้ง​ที่​แตะ file ให้​ทิ้ง​มัน​ไว้​ใน​สภาพ​ดี​กว่า​ตอน​ที่​เจอ แม้​จะ​เป็น​แค่​เปลี่ยน​ชื่อ​ตัวแปร​หนึ่ง​ตัว​หรือ​ลบ​บรรทัด​คอมเมนต์​ทิ้ง ดู Boy Scout Rule
  • Opportunistic refactoring — ตาม​แนวทาง​ของ Martin Fowler คือ​ทำ refactoring เป็น​ส่วน​หนึ่ง​ของ​งาน​ประจำ​วัน ไม่​ต้อง​รอ “sprint refactoring” เห็น code ไม่​ชัดเจน​ตอน​ไหน​ก็​แก้​ตรง​นั้น​ทันที
  • ซ่อม​ทันที หรือ “board up” ให้​ชัดเจน — ถ้า​แก้​ตอน​นี้​ไม่​ได้​จริง ๆ ให้​ทำ​เครื่องหมาย​ให้​เห็น​ชัด เช่น throw new NotImplementedException(), คอมเมนต์ TODO ที่​ผูก​กับ​หมายเลข ticket จริง หรือ failing test ที่​ระบุ​ปัญหา​ไว้ — อย่า​ปล่อย​ให้​หน้าต่าง​แตก​เนียน​หาย​ไป​ใน code จน​ดูเหมือน​เป็น​เรื่อง​ปกติ
  • ตกลง coding convention ร่วม​กัน​และ​บังคับ​ด้วย​เครื่องมือ — ใช้ linter, formatter, และ code review เป็น​ด่าน​กัน​ไม่​ให้​หน้าต่าง​แตก​บาน​ใหม่​หลุด​เข้า codebase
  • Collective Code Ownership — เมื่อ​ทุก​คน​รู้สึก​เป็น​เจ้าของ code ทั้ง​ก้อน​ร่วม​กัน การ​เห็น​ปัญหา​แล้ว​เดิน​ผ่าน​ไป​เฉย ๆ จะ​เกิด​ขึ้น​น้อย​ลง ดู Collective Code Ownership
  • จัดสรร​เวลา​สำหรับ refactor อย่าง​สม่ำเสมอ — อย่า​ปล่อย​ให้ technical debt สะสม​จน​ต้อง​หยุด​ทั้ง​ทีม​มา “จ่าย​หนี้” ที​เดียว​ใน​ภายหลัง ดู Refactoring