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

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 ควร​รับผิดชอบ

หลักการ​แก้ 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);
}
}