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
ทางแก้และการ refactor
หัวข้อที่มีชื่อว่า “ทางแก้และการ refactor”- 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
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Boy Scout Rule
- Collective Code Ownership
- Code Readability
- Refactoring
- Big Ball of Mud
- Inconsistency (code smell)