Duct Tape Coder
ปะ code ให้ใช้ได้เฉพาะหน้า โดยไม่สนคุณภาพหรือการดูแลรักษา
ปัญหาคืออะไร
หัวข้อที่มีชื่อว่า “ปัญหาคืออะไร”Duct Tape Coder คือคนที่ ปะซอฟต์แวร์ให้แก้ปัญหาเฉพาะหน้าได้ โดยไม่ใส่ใจคุณภาพหรือการดูแลรักษา ชื่อนี้มาจากคำว่า “duct tape programmer” ที่ Joel Spolsky บัญญัติไว้ในบทความชื่อดังปี 2009 ของเขา โดยเขายกย่องโปรแกรมเมอร์แบบนี้ว่าเป็นคนที่ “ship the fucking thing” — ส่งมอบของจริงออกไปได้ แทนที่จะนั่งขัดเกลา code ในห้องแล็บไม่รู้จบ เขาบอกว่าคนกลุ่มนี้จงใจหลีกเลี่ยงเทคนิคที่ซับซ้อนอย่าง multiple inheritance, multithreading หรือ COM/CORBA เพราะเชื่อว่าอะไรที่ซับซ้อนเกินไปมีแต่จะทำให้ project พัง
ปัญหาคือ Spolsky พูดถึง “duct tape” ในแง่บวก แต่สิ่งที่ตามมาในโลกจริงกลับกลายเป็นแง่ลบเสียมาก เพราะคำอธิบายของเขาผูกติดกับสมมติฐานที่ว่าคนคนนั้น “เก่งมากพอ” จะรู้ว่าจุดไหนลัดได้และจุดไหนลัดไม่ได้ แต่ในทีมจริง คำว่า “duct tape” มักถูกใช้เป็นข้ออ้างให้กับพฤติกรรมที่แท้จริงแล้วคือ ความประมาทเชิงวิชาชีพ: ไม่เขียนเทส ไม่แยกความรับผิดชอบ ไม่จัดการ error อย่างเหมาะสม และไม่ทิ้งร่องรอยให้คนอื่นเข้าใจ code ได้ ยิ่งไปกว่านั้น การปะแก้แบบนี้มักไม่ใช่ทางเลือกเดียวครั้งเดียว แต่กลายเป็น “วิธีทำงานประจำ” ที่สะสมความเสี่ยงไปเรื่อย ๆ จนถึงจุดที่ระบบเปราะบางเกินจะแก้ไขเพิ่มได้อีก
ทำไมถึงดูน่าใช้
หัวข้อที่มีชื่อว่า “ทำไมถึงดูน่าใช้”- ผลลัพธ์มองเห็นได้ทันที — feature ทำงานได้ ลูกค้าพอใจ ผู้จัดการชม สิ่งเหล่านี้เกิดขึ้นเร็วกว่าการออกแบบที่ถูกต้อง
- “Worse is Better” — Richard P. Gabriel เคยเขียนไว้ตั้งแต่ปี 1989 ว่าซอฟต์แวร์ที่ “ง่าย ๆ แต่ใช้ได้ก่อน” มักเอาชนะซอฟต์แวร์ที่ “สมบูรณ์แบบแต่มาทีหลัง” ในตลาดจริง เพราะผู้ใช้กลุ่มแรกจะสร้าง lock-in ให้กับ design ที่ยังไม่สมบูรณ์นั้น แนวคิดนี้ถูกอ้างซ้ำแล้วซ้ำเล่าเพื่อสร้างความชอบธรรมให้กับการลัดคุณภาพ
- วัฒนธรรมฮีโร่ — ทีมที่มีเดดไลน์บีบมักให้รางวัลคนที่ “ดับไฟ” ได้เร็ว มากกว่าคนที่ป้องกันไฟตั้งแต่แรก คนที่ปะ code สำเร็จจึงถูกมองว่าเป็นฮีโร่ ทั้งที่จริงเขาอาจกำลังสร้างไฟกองใหญ่กว่าเดิมไว้รอวันข้างหน้า
- ดูขัดแย้งกับ “over-engineering” น้อยกว่า — เมื่อเทียบกับ Architecture Astronaut ที่ใช้เวลาออกแบบไม่รู้จบแต่ไม่เคยส่งมอบ การลงมือทำทันทีย่อมดูเป็นทางเลือกที่ “สมเหตุสมผลกว่า” ในสายตาคนนอก
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”Martin Fowler อธิบายไว้ใน Technical Debt Quadrant ว่าหนี้ทางเทคนิคมี 2 มิติ: ตั้งใจหรือไม่ตั้งใจ (deliberate/inadvertent) และรอบคอบหรือประมาท (prudent/reckless) การตัดสินใจแบบ “เดดไลน์บีบจนต้องลัด” ที่มาพร้อมการประเมินความเสี่ยงไว้ล่วงหน้า (deliberate-prudent) ยังพอรับได้ แต่ Duct Tape Coder ส่วนใหญ่ตกอยู่ในโซน deliberate-reckless หรือแม้แต่ inadvertent-reckless — คือรู้ว่าลัดแต่ไม่ประเมินราคาที่ต้องจ่ายคืน หรือแย่กว่านั้นคือไม่รู้ตัวด้วยซ้ำว่ากำลังสร้างหนี้อยู่
Ayende Rahien วิจารณ์บทความของ Spolsky ไว้ตรง ๆ ว่า การ ship ซอฟต์แวร์ครั้งเดียวมันง่าย แต่โจทย์จริงของงานวิชาชีพคือ “การ ship ซอฟต์แวร์ต่อเนื่อง” เขายกกรณี Netscape เป็นตัวอย่าง: เบราว์เซอร์ที่ไม่เสถียรเรื้อรัง ต้องเขียน code ใหม่ทั้งหมดซึ่งกินเวลา 5-6 ปี และสุดท้ายก็แพ้ทางให้ Internet Explorer ปัญหาเชิงลึกของ Duct Tape Coder มีดังนี้
- ไม่มีเทสรองรับ — เมื่อไม่มี automated test การแก้ไขครั้งต่อไปจะเสี่ยงต่อ regression ทุกครั้ง ทำให้ทีมกลัวการเปลี่ยนแปลง (ดู Poor Tests)
- Bus factor ต่ำ — code ที่ปะแบบไม่มีโครงสร้างมักเข้าใจได้เฉพาะคนที่เขียน เมื่อคนนั้นลาออกหรือโยกย้าย ทีมที่เหลือแทบต้องเขียนใหม่ทั้งหมด
- หนี้สะสมแบบทบต้น — การลัดครั้งแรกดูเหมือนไม่มีอะไร แต่การลัดซ้ำ ๆ บนฐานที่ไม่มั่นคงจะทำให้ทุก feature ใหม่ใช้เวลานานขึ้นเรื่อย ๆ จนถึงจุดที่ความเร็วในการส่งมอบกลับช้ากว่าทีมที่ลงทุนกับคุณภาพตั้งแต่ต้น
- ปนกับ Broken Windows — เมื่อ code ส่วนหนึ่ง “ดูรก” อยู่แล้ว คนอื่นในทีมก็มักรู้สึกว่าลัดเพิ่มอีกนิดก็ไม่เป็นไร ทำให้ทั้ง codebase เสื่อมสภาพเร็วกว่าที่ควร (ดู Broken Windows)
- สับสนระหว่างเก่งกับลัด — สมมติฐานของ Spolsky ที่ว่า “ต้องเก่งมากพอ” ถึงจะลัดได้อย่างปลอดภัย ในทางปฏิบัติแทบวัดไม่ได้ล่วงหน้า ทีมจึงมักค้นพบว่าตัวเองลัดผิดที่หลังจากความเสียหายเกิดขึ้นแล้ว
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”code ต่อไปนี้เป็นตัวอย่างทั่วไปของ Duct Tape Coder ในระบบประมวลผลคำสั่งซื้อ — ทำงานได้ในเดโม แต่ซ่อนความเสี่ยงไว้เต็มไปหมด: กลืน exception ทุกชนิดแบบเงียบ ๆ, ต่อ SQL ด้วย string concatenation, ผสมตรรกะธุรกิจกับการเข้าถึงข้อมูลและการส่งอีเมลไว้ใน method เดียว และไม่มีเทสรองรับเลย
// ก่อนแก้: วิธีของ Duct Tape Coder — ปะให้ผ่าน ไม่สนโครงสร้างpublic class OrderProcessor{ public void Process(int orderId, string customerEmail, decimal amount) { try { // ต่อ SQL ตรง ๆ เสี่ยง SQL injection และแก้ยากเวลา schema เปลี่ยน var conn = new SqlConnection("Server=prod-db;Database=Orders;..."); conn.Open(); var cmd = new SqlCommand( "UPDATE Orders SET Status='PAID', Amount=" + amount + " WHERE Id=" + orderId, conn); cmd.ExecuteNonQuery();
// ส่งอีเมลแบบ hardcode ปนอยู่กับ logic การชำระเงิน var smtp = new SmtpClient("smtp.internal"); smtp.Send("orders@shop.com", customerEmail, "ชำระเงินสำเร็จ", "ขอบคุณค่ะ"); } catch (Exception) { // กลืน error ทุกชนิดแบบเงียบ ๆ — ไม่ log ไม่แจ้งใคร // "มันรันผ่านแล้วก็จบ ไม่มีเวลาคิดเรื่อง edge case" } }}การ refactor แยกความรับผิดชอบตาม SRP, ใช้ dependency injection แทนการสร้าง connection ตรง ๆ, จัดการ error อย่างเจาะจง และเปิดทางให้เขียนเทสได้
// หลังแก้: แยกชั้นความรับผิดชอบ จัดการ error อย่างเจาะจง เทสได้public class OrderProcessor{ private readonly IOrderRepository _orders; private readonly IPaymentNotifier _notifier; private readonly ILogger<OrderProcessor> _logger;
public OrderProcessor( IOrderRepository orders, IPaymentNotifier notifier, ILogger<OrderProcessor> logger) { _orders = orders; _notifier = notifier; _logger = logger; }
public async Task ProcessAsync(OrderId orderId, Money amount) { try { var order = await _orders.GetAsync(orderId) ?? throw new OrderNotFoundException(orderId);
order.MarkAsPaid(amount); await _orders.SaveAsync(order); await _notifier.NotifyPaymentSucceededAsync(order); } catch (OrderNotFoundException ex) { _logger.LogWarning(ex, "ไม่พบคำสั่งซื้อ {OrderId}", orderId); throw; } catch (SqlException ex) { _logger.LogError(ex, "บันทึกคำสั่งซื้อ {OrderId} ไม่สำเร็จ", orderId); throw new OrderProcessingException(orderId, ex); } }}ผลลัพธ์: แต่ละส่วนเทสแยกกันได้ (mock IOrderRepository, IPaymentNotifier), error ที่เกิดขึ้นจริงถูก log และไม่หายไปเงียบ ๆ และเมื่อ schema หรือผู้ให้บริการอีเมลเปลี่ยน ก็แก้แค่จุดเดียวโดยไม่กระทบตรรกะธุรกิจ
ทางแก้และการ refactor
หัวข้อที่มีชื่อว่า “ทางแก้และการ refactor”แผนภาพด้านล่างแสดงวงจรที่ Duct Tape Coder มักติดอยู่ และจุดที่ควรตัดวงจรนั้น
flowchart TD A[เดดไลน์บีบ] --> B[เขียน code ลัดขั้นตอน] B --> C[ส่งมอบได้เร็ว] C --> D[ได้รับคำชมว่าเก่ง] D --> A B --> E[ไม่มี tests ไม่มี design] E --> F[หนี้ทางเทคนิคสะสม] F --> G[แก้ไขแต่ละจุดยากขึ้นเรื่อย] G --> H[ระบบเปราะจนต้อง rewrite ทั้งหมด]
แนวทางที่สมดุลกว่าไม่ใช่การห้ามลัดคุณภาพเด็ดขาด แต่คือทำให้ทุกการลัดเป็น deliberate & prudent ตาม Technical Debt Quadrant ของ Fowler:
- ตั้งใจก่อนลัด ไม่ใช่ลัดโดยไม่รู้ตัว — ถ้าจำเป็นต้องข้ามเทสหรือ design ที่ดีเพื่อเดดไลน์ ให้บันทึกไว้ชัดเจน (ticket/comment) ว่าลัดตรงไหนและทำไม เพื่อให้ทีมกลับมาจ่ายคืนได้ ไม่ใช่ลืมไปเลย
- refactor เป็นนิสัย ไม่ใช่ project แยก — ใช้ Refactoring และ Boy Scout Rule คือ “ทิ้ง code ให้สะอาดกว่าตอนที่เจอ” ทุกครั้งที่แตะ file เดิม แทนที่จะรอ “sprint ทำความสะอาด” ที่มักไม่มีวันมาถึง (ดู Boy Scout Rule)
- มีเทสก่อนขยาย code เก่า — Test-Driven Development ช่วยให้มั่นใจว่าการแก้ไขในอนาคตไม่ทำลายพฤติกรรมเดิมโดยไม่รู้ตัว แม้จะเริ่มจากเทสแบบ characterization test บน code ที่ยังไม่สะอาดก็ตาม
- แยกวงจรฟีดแบ็กจากแรงกดดันเดดไลน์ — ปัญหาจริงมักไม่ใช่ตัวโปรแกรมเมอร์ แต่คือวัฒนธรรมที่ให้รางวัลความเร็วอย่างเดียว ผู้จัดการควรวัดผลจาก sustainable pace ไม่ใช่จำนวนไฟที่ดับได้ต่อสัปดาห์ ไม่เช่นนั้นทีมจะเข้าสู่ Death March ในระยะยาว
- อย่าแกว่งไปอีกสุดขั้ว — คำตอบไม่ใช่การออกแบบทุกอย่างล่วงหน้าจนไม่ได้ส่งมอบ (Fast Beats Right เตือนไว้อีกด้าน) เป้าหมายคือ YAGNI — ทำเท่าที่จำเป็นแต่ทำให้ถูกต้อง ไม่ใช่ทำน้อยแบบไม่รับผิดชอบ