Golden Hammer
“ถ้ามีแต่ค้อน ทุกอย่างก็ดูเป็นตะปู”
ปัญหาคืออะไร
หัวข้อที่มีชื่อว่า “ปัญหาคืออะไร”Golden Hammer หมายถึงภาษา เครื่องมือ framework หรือ platform ที่นักพัฒนาหรือทีมถนัดและทำงานได้ดีมาก จนถูกล่อใจให้ ใช้มันกับทุกปัญหาที่เจอ โดยไม่ทันหยุดถามว่ามันเหมาะกับปัญหานั้นจริงหรือไม่
ชื่อนี้มาจากแนวคิดทางจิตวิทยาที่เรียกว่า law of the instrument ซึ่ง Abraham Kaplan เขียนไว้ตั้งแต่ปี 1964 ว่า “ให้ค้อนกับเด็กชายตัวเล็ก ๆ แล้วเขาจะพบว่าทุกสิ่งที่เจอต้องถูกตอกทั้งนั้น” ต่อมา Abraham Maslow นำไปสรุปเป็นวรรคทองในปี 1966 ว่า
“ถ้าสิ่งเดียวที่คุณมีคือค้อน ทุกอย่างก็ดูเหมือนตะปูไปหมด”
ในวงการซอฟต์แวร์ คำว่า golden hammer ถูกบันทึกไว้ในวรรณกรรม antipattern อย่างเป็นทางการตั้งแต่ปี 1998 ในฐานะ “เทคโนโลยีหรือแนวคิดที่คุ้นเคย ถูกนำไปใช้อย่างหมกมุ่นกับปัญหาซอฟต์แวร์แทบทุกชนิด” ตัวอย่างคลาสสิกคือการยัด logic ที่ควรอยู่ในชั้น application ไว้ในฐานข้อมูลแทน เพราะนักพัฒนาถนัด SQL มากกว่าภาษา app หรือในทางกลับกัน การบังคับให้ทุกอย่างเป็น object-oriented ทั้งที่บางส่วนควรเป็นแค่ script ง่าย ๆ
ทำไมถึงดูน่าใช้
หัวข้อที่มีชื่อว่า “ทำไมถึงดูน่าใช้”Golden Hammer ไม่ได้เกิดจากความขี้เกียจอย่างเดียว แต่มีเหตุผลที่ฟังดูสมเหตุสมผลอยู่เบื้องหลัง:
- ความเร็วในระยะสั้น — เครื่องมือที่ถนัดทำให้ส่งงานได้เร็ว ไม่ต้องเสียเวลาเรียนรู้ของใหม่ท่ามกลาง deadline
- ความเชี่ยวชาญสะสม — ยิ่งใช้เครื่องมือเดิมนานเท่าไหร่ ก็ยิ่งเก่งขึ้นเรื่อย ๆ จนรู้สึกว่ามันแก้ได้ “ทุกอย่าง” จริง ๆ
- ต้นทุนจม (sunk cost) — ถ้าองค์กรลงทุนซื้อ license หรือฝึกทีมกับ toolkit ใดไปมากแล้ว ก็มีแรงกดดันให้ต้อง “คุ้มค่า” ด้วยการใช้มันกับทุก project ไม่ว่าจะเหมาะสมหรือไม่
- ความสบายใจ (comfort zone) — การเรียนรู้เทคโนโลยีใหม่มีความเสี่ยงและใช้พลังงาน สมองจึงเลือกเส้นทางที่คุ้นเคยโดยอัตโนมัติ
- ดูเหมือนเป็นมาตรฐานเดียวทั้งองค์กร — ผู้บริหารบางคนมองว่าการใช้เครื่องมือเดียวทุกที่ช่วยลดความหลากหลายและง่ายต่อการจ้างคน ซึ่งดูดีในทางทฤษฎี
ปัญหาคือเหตุผลเหล่านี้ล้วนมองจาก “ความสะดวกของผู้เลือก” ไม่ใช่จาก “ความเหมาะสมกับปัญหา”
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”สัญญาณเตือนที่บ่งบอกว่าทีมติดกับดัก Golden Hammer มักมีลักษณะร่วมกันดังนี้
- สถาปัตยกรรมของระบบถูกอธิบายด้วยชื่อ vendor หรือ toolkit เดียวเสมอ แทนที่จะอธิบายด้วยปัญหาที่ระบบแก้
- ทีมทำ requirement บางอย่างไม่สำเร็จซ้ำ ๆ เพราะพยายามบิดมันให้เข้ากับเครื่องมือเดิม แทนที่จะยอมรับว่าต้องใช้แนวทางอื่น
- ทีมไม่เคยสำรวจเทคโนโลยีหรือแนวทางใหม่เลยเป็นเวลานาน
- วงจรชีวิตของการพัฒนาแทบทั้งหมดผูกติดกับ vendor หรือ stack เดียว
ผลที่ตามมาคือ ความซับซ้อนเทียม (accidental complexity) — ต้องดัดโครงสร้างข้อมูลหรือ logic ให้บิดเบี้ยวเพื่อให้พอดีกับเครื่องมือที่ไม่ได้ออกแบบมาสำหรับงานนั้น, ประสิทธิภาพตกต่ำ เพราะใช้เครื่องมือผิดจุดประสงค์ (เช่นยัด business rule ที่เปลี่ยนบ่อยไว้ใน stored procedure ที่ deploy ยาก), ทดสอบยาก เพราะ logic ปนอยู่กับเทคโนโลยีเฉพาะทาง, และ ต้นทุนเปลี่ยนแปลงสูงขึ้นเรื่อย ๆ เพราะทุกอย่างผูกกับเครื่องมือเดียวจนแยกออกไม่ได้ — สุดท้ายกลายเป็น vendor lock-in โดยไม่ได้ตั้งใจ
ตัวอย่างร่วมสมัยที่เห็นบ่อยคือทีมที่ยึด microservices เป็น golden hammer แล้วสับระบบเล็ก ๆ ที่มี traffic ต่ำให้กลายเป็นหลาย10 service เพราะเชื่อว่านั่นคือ “แนวทางที่ถูกต้องเสมอ” ผลคือ latency จากการเรียกข้ามเครือข่ายเพิ่มขึ้น, ความซับซ้อนของ deployment และ monitoring พุ่งสูง ทั้งที่ monolith ธรรมดาก็เพียงพอ
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”สมมติทีมหนึ่งถนัด T-SQL มาก จนย้าย business logic ที่ควรอยู่ใน domain model ไปไว้ใน stored procedure ทั้งหมด รวมถึง logic ที่เปลี่ยนบ่อยอย่างการคำนวณส่วนลดคำสั่งซื้อ
// แอนตี้แพทเทิร์น: business logic ที่เปลี่ยนบ่อยถูกฝังไว้ใน stored procedure// เพราะทีมถนัด SQL มากกว่า C# — ทดสอบยาก, deploy แยกจาก code, versioning ลำบากpublic class OrderService{ private readonly SqlConnection _connection;
public void PlaceOrder(int customerId, int orderId) { using var command = new SqlCommand("sp_PlaceOrder_ApplyAllBusinessRules", _connection) { CommandType = CommandType.StoredProcedure }; command.Parameters.AddWithValue("@CustomerId", customerId); command.Parameters.AddWithValue("@OrderId", orderId);
// ภายใน sp_PlaceOrder_ApplyAllBusinessRules มีทั้งการคำนวณส่วนลด, // ตรวจสต็อก, กำหนดราคาสมาชิก VIP และเงื่อนไขโปรโมชันนับสิบข้อ // ทุกครั้งที่การตลาดเปลี่ยนกติกา ต้องแก้และ deploy SQL script แยกต่างหาก command.ExecuteNonQuery(); _connection.Close(); }}เมื่อทีมนี้ต้องเขียน unit test ให้ business rule กลับทำไม่ได้เลยเพราะ logic ทั้งหมดอยู่หลังกำแพงของฐานข้อมูล ต้องพึ่ง integration test ที่ต่อ database จริงเท่านั้น และทุกครั้งที่กติกาส่วนลดเปลี่ยน ต้องประสานงาน deploy SQL แยกจาก application — เป็นสัญญาณชัดว่าเครื่องมือ (SQL) ถูกใช้เกินขอบเขตที่มันควรทำ (เก็บและดึงข้อมูล) ไปทำหน้าที่ที่ domain model ควรรับผิดชอบ
ทางแก้และการ refactor
หัวข้อที่มีชื่อว่า “ทางแก้และการ refactor”หลักการแก้ Golden Hammer ไม่ใช่การห้ามใช้เครื่องมือที่ถนัด แต่คือการ เลือกเครื่องมือให้เหมาะกับปัญหา โดยเริ่มจาก เข้าใจว่ากำลังแก้ปัญหาอะไร ก่อนเลือกวิธี ไม่ใช่เลือกวิธีก่อนแล้วค่อยบิดปัญหาให้เข้ากับวิธีนั้น
แนวทางปฏิบัติที่ช่วยได้:
- แยก policy ออกจาก mechanism — ย้าย business rule ที่เปลี่ยนบ่อยกลับมาไว้ใน domain model ที่ทดสอบง่าย ใช้ฐานข้อมูลทำหน้าที่เก็บและดึงข้อมูลเป็นหลัก
- ทำ proof-of-concept เล็ก ๆ ก่อนเลือก — เปรียบเทียบข้อดีข้อเสียของแต่ละทางเลือกด้วยของจริง ไม่ใช่ความเชื่อ
- ขยายขอบเขตการเรียนรู้ของทีม — อ่านงานของทีมอื่น ติดตาม project open source ไปงานสัมมนา เพื่อไม่ให้ตัวเลือกในหัวมีอยู่แค่ตัวเดียว
- ตั้งขอบเขตชัดเจนสำหรับเทคโนโลยีใหม่ — เปิดพื้นที่ทดลองเทคโนโลยีอื่นในจุดที่ผลกระทบต่ำ แทนที่จะห้ามหรือบังคับทั้งระบบ
- ยึด KISS — เครื่องมือที่เหมาะสมมักเป็นเครื่องมือที่ง่ายที่สุดที่แก้ปัญหาได้พอดี ไม่ใช่เครื่องมือที่ทรงพลังที่สุดที่ทีมมี
ต่อไปนี้คือ logic เดียวกันหลัง refactor — ย้ายกฎการคำนวณส่วนลดมาไว้ใน domain model ที่ทดสอบได้โดยตรง และใช้ SQL เพียงเพื่อโหลด/บันทึกข้อมูลเท่านั้น
// refactor: business logic ย้ายเข้า domain model ทดสอบง่าย// SQL/repository ทำหน้าที่แค่ persist ข้อมูล ไม่ต้องรู้เรื่องกฎส่วนลดpublic class Order{ public decimal Subtotal { get; private set; } public bool CustomerIsVip { get; private set; }
public decimal CalculateDiscount() { // กฎที่เปลี่ยนบ่อยอยู่ใน C# ทดสอบด้วย unit test ธรรมดาได้ทันที if (CustomerIsVip && Subtotal > 1000m) { return Subtotal * 0.15m; } return Subtotal > 500m ? Subtotal * 0.05m : 0m; }}
public class OrderService{ private readonly IOrderRepository _orders;
public OrderService(IOrderRepository orders) => _orders = orders;
public void PlaceOrder(Order order) { var discount = order.CalculateDiscount(); // repository แค่บันทึกผลลัพธ์ ไม่มี business rule ฝังอยู่ _orders.Save(order, discount); }}