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

Calendar Coder

ทำ​ตาม​คำ​แนะนำ​อย่าง​งมงาย​โดย​ไม่​เข้าใจ​ว่า​ทำไม (Cargo Cult)

Calendar Coder เป็นการ​เล่นคำ​กับ​แนวคิด cargo cult programming — ชื่อ​นี้​มา​จาก​ปฏิทิน Software Craftsmanship AntiPatterns ปี 2012 และ​ล้อ​ว่า​นัก​พัฒนา​บาง​คน “เขียน code ตาม​ที่​ปฏิทิน​บอก” คือ​ทำ​ตาม​คำ​แนะนำ​ที่​เห็น​มา​โดย​ไม่​เข้าใจ​เหตุผล​เบื้องหลัง แก่น​ของ​ปัญหา​คือ การ​ขาด​ความคิด​และ​ความ​เข้าใจ ของ​นัก​พัฒนา​ที่​ทำ​ตาม​คำ​แนะนำ​จาก​แหล่ง​ที่​ตน​เชื่อ​ว่า​น่า​เชื่อถือ​อย่าง​งมงาย

คำ​ว่า cargo cult มี​ราก​มา​จาก “cargo cult science” ที่ Richard Feynman พูด​ถึง ซึ่ง​อ้างอิง​ถึง​ลัทธิ​บูชา​สินค้า​บน​เกาะ​แถบ​เม​ลา​นีเซีย​หลัง​สงครามโลก​ครั้ง​ที่​สอง ชาว​เกาะ​เห็น​ว่า​เครื่องบิน​ทหาร​เคย​นำ​สิ่งของ​มา​ลง​เมื่อ​มี​รันเวย์ หอ​บังคับการ และ​คน​ใส่​หู​ฟัง พวก​เขา​จึง​สร้าง​รันเวย์​จำลอง หอ​ไม้ไผ่ และ​หู​ฟัง​ไม้​ขึ้น​มา แล้ว​รอ​ให้​เครื่องบิน​กลับ​มา — แต่​มัน​ไม่​มา​อีก​เลย Feynman สรุป​ไว้​ว่า “รูปแบบ​สมบูรณ์​แบบ มัน​ดูเหมือน​ของ​จริง​ทุก​อย่าง แต่​มัน​ไม่​ทำงาน” นี่​คือ​หัวใจ​ของ​ปัญหา: ลอก​รูปแบบ​ภายนอก​ของ​สิ่ง​ที่​ได้​ผล โดย​ไม่​เข้าใจ​หลักการ​ที่​ทำให้​มัน​ได้​ผล​จริง

ใน​โลก code อาการ​นี้​คือ​การ​ใส่ code หรือ​โครงสร้าง​เข้า​มา​แบบ​เป็น​พิธีกรรม ทั้ง​ที่​ไม่​ได้​ทำ​หน้าที่​อะไร​จริง ๆ มัก​เกิด​กับ​คน​ที่​ไม่​เข้าใจ​ทั้ง bug ที่​ตัวเอง​พยายาม​แก้ และ​ไม่​เข้าใจ​ทาง​แก้​ที่​ตัวเอง​คัด​ลอก​มา คำ​ว่า cargo cult programming ปรากฏ​ครั้ง​แรก​ใน Jargon File version 2.5.1 ตั้งแต่​เดือน​มกราคม 1991 และ​ยัง​ใช้​อธิบาย​การ​หยิบ design pattern หรือ​สไตล์​การ​เขียน​มา​ใช้​อย่าง​มืด​บอด​โดย​ไม่​เข้าใจ​ว่า​หลักการ​นั้น​มี​ไว้​แก้​ปัญหา​อะไร

  • มัน​มักได้​ผล​ใน​ตอน​แรก — code ที่​คัด​ลอก​มา​จาก​คำ​ตอบ​ใน Stack Overflow หรือ block ที่​ดู​น่า​เชื่อถือ มัก​รัน​ผ่าน​ทันที​ใน​เดโม ความ​สำเร็จ​ที่​มอง​เห็น​ได้​เร็ว​นี้​เอง​ที่​ตอกย้ำ​พฤติกรรม​ให้​ทำซ้ำ
  • ดูเหมือน “best practice” — เมื่อ​ทีม​อื่น​หรือ​ซีเนียร์​ทำ​แบบ​นี้ การ​ทำ​ตาม​ย่อม​รู้สึก​ปลอดภัย​กว่า​การ​ตั้ง​คำถาม การ​เลียน​แบบ​รูปแบบ​ที่​คน​เก่ง ๆ ใช้ ทำให้​รู้สึก​ว่า​ตัวเอง​กำลัง​เขียน code อย่าง​มือ​อาชีพ
  • ประหยัด​แรง​คิด​ใน​ระยะ​สั้น — การ​เข้าใจ​ว่า ทำไม บาง​อย่าง​ถึง​ได้​ผล​นั้น​ใช้​พลังงาน​มากกว่า​กา​รก็อปมา​วาง​เฉย ๆ ภาย​ใต้​เดดไลน์ ทาง​ลัด​ทาง​ความคิด​นี้​จึง​น่า​ดึงดูด​เสมอ
  • AI ยิ่ง​เร่ง​ให้​เกิด​ง่าย​ขึ้น — เครื่องมือ​ช่วย​เขียน code ด้วย LLM เลียน​แบบ​โครงสร้าง code ที่ “ดู​เป็น​มือ​อาชีพ” ได้​อย่าง​คล่องแคล่ว เพราะ​มัน​เคย​เห็น​รูปแบบ​เหล่า​นั้น​ซ้ำ ๆ ใน​ข้อมูล​ฝึก ผล​คือ​ความ​ซับซ้อน​ที่​ไม่​จำเป็น​กลับ​ดู “ถูกต้อง” และ​ถูกรับ​มา​โดย​ไม่​ตั้ง​คำถาม

ปัญหา​ที่​ลึก​ที่สุด​คือ ถ้า​คุณ​ไม่รู้​ว่า​ทำไม code ถึง​ทำงาน คุณ​ก็​จะ​ไม่รู้​ว่า​ทำไม​มัน​ถึง​พัง แนวคิด​นี้​ตรง​กับ​สิ่ง​ที่ The Pragmatic Programmer เรียก​ว่า “programming by coincidence” หรือ​การ​เขียน​โปรแกรม​ด้วย​ความ​บังเอิญ — code ใช้ได้ แต่​ไม่มี​เหตุผล​รองรับ​ว่า​ทำไม เมื่อ​บริบท​เปลี่ยน​ไป​เพียง​เล็กน้อย มัน​ก็​พัง​โดยที่​เจ้าของ code ไล่​หา​สาเหตุ​ไม่​เจอ หนังสือ​เล่ม​นั้น​แยก​ความ​บังเอิญ​ออก​เป็น​สอง​แบบ:

  • Accident of implementation — พึ่งพา​พฤติกรรม​ที่​ไม่​ได้​เอกสาร​ระบุ เช่น side effect ที่​บังเอิญ​เกิด​เมื่อ​เรียก method ตาม​ลำดับ​หนึ่ง หรือ parameter ที่​ไม่​ได้​ตั้ง​ค่า​เริ่มต้น​แต่​บังเอิญ​เป็น​ค่าที่​ต้องการ เมื่อ library อัปเดต code ก็​พัง​ทันที
  • Accident of context — เขียน code โดย​ตั้ง​สมมติฐาน​ที่​ไม่​ได้​ตรวจสอบ เช่น สมมติ​ว่า​จะ​มี GUI อยู่​เสมอ หรือ​คัด​ลอก code ข้าม​บริบท​โดย​ไม่​เข้าใจ​ทั้ง​ต้นทาง​และ​ปลายทาง

ผล​เสีย​ที่​ตาม​มา​เป็น​รูปธรรม:

  • code บวม​โดย​ไม่มี​ประโยชน์ — ริทัวล​ที่ก็อปมา​สะสม​เป็น​ชั้น ๆ เช่น ห่อ webhook receiver ธรรมดา ๆ ด้วย repository, factory และ message queue ทั้ง​ที่​โจทย์​จริง​ต้องการ​แค่​ตรวจ signature กับ​บันทึก​ข้อมูล ความ​ซับซ้อน​ที่​ไม่​จำเป็น​ทำให้​อ่าน​ยาก เทส​ยาก และ​แก้​ยาก​ขึ้น​ทุก​ครั้ง
  • bug ที่​อันตราย​กว่า​เดิม — กา​รก็อป pattern มา​ใช้​ผิด​ที่​ทำให้​เกิด​ข้อ​ผิดพลาด​ร้ายแรง เช่น เอา retry wrapper ไป​ครอบ operation ที่​ไม่ idempotent จน​เกิด​การ​เรียก​เก็บ​เงิน​ซ้ำ​ใน​โปร​ดัก​ชัน
  • ความ​เข้าใจ​ผิด​เชิง​องค์กร — Steve McConnell เรียก​อาการ​ระดับ​ทีม​ว่า “cargo cult software engineering” คือ​องค์กร​เลียน​แบบ​วิธี​ทำงาน​ของ​ทีม​ที่​ประสบ​ความ​สำเร็จ โดย​สับสน​ระหว่าง​เหตุ​กับ​ผล เช่น สั่ง​ให้​ทุก​คน​ทำงาน​ดึก​เพราะ​เห็น​ว่า​ทีม​เก่ง ๆ ทำงาน​ดึก ทั้ง​ที่​การ​ทำงาน​ดึก​เป็น ผล ของ​แรง​จูงใจ ไม่ใช่ เหตุ ของ​ความ​สำเร็จ
  • สะสม​เป็น​ธรรมเนียม​ปลอม — เมื่อ​ริทัวล​ถูก​ทิ้ง​ไว้​ใน codebase คน​รุ่น​ถัด​ไป (รวม​ถึง AI agent) จะ​ถือว่า​มัน​เป็น “แบบแผนที่​ทีม​ยึดถือ” แล้ว​ทำ​ตาม ทำให้​ความ​เข้าใจ​ผิด​ขยาย​ตัว​และ​ฝัง​ลึก​ลง​เรื่อย ๆ

code ต่อ​ไป​นี้​เต็ม​ไป​ด้วย​ริทัวล​ที่​คัด​ลอก​มา​โดย​ไม่​เข้าใจ ทุก​บรรทัด “ดูเหมือน code ที่​ดี” แต่​แต่ละ​บรรทัด​กลับ​สร้าง​ปัญหา​แทนที่​จะ​แก้

// ก่อนแก้: ริทัวลที่ก็อปมาโดยไม่เข้าใจว่าแต่ละบรรทัดแก้ปัญหาอะไร
public class UserService
{
private readonly IUserRepository _repo;
public User GetUser(int id)
{
// "เห็นใน template เขาห่อ Task.Run กันหมด เลยห่อตาม"
// จริง ๆ คือ block thread เปล่า ๆ และเสี่ยง deadlock
var user = Task.Run(() => _repo.Find(id)).Result;
// "อ่าน block มาว่าเรียก GC แล้ว app จะเบา"
// จริง ๆ คือฝืนให้ garbage collector ทำงานหนักขึ้นโดยไม่จำเป็น
GC.Collect();
GC.WaitForPendingFinalizers();
if (user == null)
{
try
{
throw new Exception("not found");
}
catch (Exception ex)
{
// "ที่อื่นเห็น throw ex กัน เลยทำตาม"
// จริง ๆ คือรีเซ็ต stack trace เดิมทิ้ง ทำให้ debug ยากขึ้น
throw ex;
}
}
return user;
}
}

เมื่อ​ถาม​ว่า “ทำไม​ต้อง​มี​บรรทัด​นี้” คำ​ตอบ​คือ “เพราะ​เห็น​คน​อื่น​ทำ” — นั่น​คือ​สัญญาณ​ของ cargo cult ที่​ชัดเจน​ที่สุด version ที่​แก้​แล้ว​เก็บ​เฉพาะ​สิ่ง​ที่​มี​เหตุผล​รองรับ และ​ตัด​พิธีกรรม​ที่​ไม่มี​จุด​ประสงค์​ทิ้ง​ทั้งหมด

// หลังแก้: เก็บเฉพาะสิ่งที่อธิบายได้ว่าทำไม ตัดริทัวลที่ไร้จุดประสงค์
public class UserService
{
private readonly IUserRepository _repo;
public UserService(IUserRepository repo) => _repo = repo;
public async Task<User> GetUserAsync(int id)
{
// async ตลอดสาย ไม่ block thread และไม่ต้องพึ่ง Task.Run
var user = await _repo.FindAsync(id);
// ไม่ยุ่งกับ GC — ปล่อยให้ runtime จัดการหน่วยความจำตามที่มันถูกออกแบบมา
if (user is null)
throw new UserNotFoundException(id); // exception เจาะจง รักษา stack trace เดิมไว้
return user;
}
}

หลัก​ที่​ใช้​ตัดสิน​ไม่ใช่ “code สั้น​ลง” แต่​คือ ทุก​บรรทัด​ที่​เหลือ​อยู่​ตอบ​ได้​ว่า​มัน​แก้​ปัญหา​อะไร ถ้า​ตอบ​ไม่​ได้ แปล​ว่า​มัน​คือ​ริทัวล​ที่​ควร​ลบ

แผนภาพ​ด้าน​ล่าง​แสดง​จุด​ตัดสิน​ใจ​ที่​แยก​ระหว่าง Calendar Coder กับ​นัก​พัฒนา​ที่​เขียน code อย่าง​ตั้งใจ (program deliberately)

flowchart TD
  A[เจอ code หรือคำแนะนำที่ดูน่าเชื่อถือ] --> B[คัดลอกมาใช้ตามรูปแบบ]
  B --> C{เข้าใจไหมว่าทำไมมันได้ผล}
  C -->|ไม่เข้าใจ| D[ใส่ริทัวลที่ไม่มีจุดประสงค์]
  D --> E[code บวมและเปราะ]
  E --> F[พังเมื่อบริบทเปลี่ยนแต่ไล่หาสาเหตุไม่เจอ]
  C -->|เข้าใจ| G[เก็บเฉพาะส่วนที่แก้ปัญหาจริง]
  G --> H[code เรียบง่ายและอธิบายได้]

แนวทาง​แก้​ไม่ใช่​การ​ห้าม​ลอก code หรือ​ห้าม​ฟัง​คำ​แนะนำ แต่​คือ​การ​ใส่ วิจารณญาณ เข้าไป​ทุก​ครั้ง:

  • ใช้​วิจารณญาณ​กับ​ทุก​แหล่ง — ไม่​ว่า​คำ​แนะนำ​จะ​มา​จาก block ดัง ซีเนียร์​ใน​ทีม หรือ AI ให้​ถาม​เสมอ​ว่า ทำไม และ อย่างไร จึง​ควร (หรือ​ไม่​ควร) ทำ​สิ่ง​นั้น นี่​คือ​หัวใจ​ที่ DevIQ เน้น และ​เป็น​เส้น​แบ่ง​เดียว​กับ Found on Internet
  • ตั้ง​ชื่อ​แรง​ที่ pattern นั้น​แก้ — ก่อน​รับ pattern หรือ​โครงสร้าง​ใด ให้​ระบุ​ให้​ได้​ว่า​มัน​แก้​ปัญหา​อะไร ใน codebase ของ​เรา ไม่ใช่​แก้​ปัญหา​ทั่ว ๆ ไป​ใน​ทฤษฎี ถ้า​ตอบ​ไม่​ได้ ก็​ยัง​ไม่​ควร​ใส่​มัน​เข้า​มา
  • ทำ deletion test — ลอง​ลบ pattern หรือ​บรรทัด​ที่​สงสัย​ออก แล้ว​รัน​เทส ถ้า​เทส​ยัง​ผ่าน​และ code เรียบ​ง่าย​ขึ้น แสดง​ว่า​สิ่ง​นั้น​คือ​ริทัวล​ที่​ไม่​จำเป็น
  • พิสูจน์ invariant ด้วย​เทส — ถ้า pattern จำเป็น​จริง ให้​เขียน​เทส​ที่​พิสูจน์​ว่า​ทำไม​มัน​ถึง​ต้อง​อยู่​ตรง​นั้น เทส​จะ​กลาย​เป็น​เอกสาร​ที่​อธิบาย​เหตุผล​ให้​คน​รุ่น​ถัด​ไป
  • เขียน​โปรแกรม​อย่าง​ตั้งใจ — ตาม​คำ​แนะนำ​ของ The Pragmatic Programmer: จง​อธิบาย code ของ​ตัวเอง​ให้​จูเนียร์​ฟังได้​อย่าง​ละเอียด ถ้า​อธิบาย​ไม่​ได้ แปล​ว่า​คุณ​กำลัง​พึ่ง​ความ​บังเอิญ ให้​ทดสอบ​สมมติฐาน​แทนที่​จะ​เดา
  • ยึด​ความ​เรียบ​ง่าย​เป็น​ค่า​ตั้งต้น — เมื่อ​ไม่​แน่ใจ ให้​เลือก​ทาง​ที่​เรียบ​ง่าย​ที่สุด​ที่​ยัง​แก้​ปัญหา​ได้ (KISS) แล้ว​ค่อย refactor เพิ่ม​ความ​ซับซ้อน​เมื่อ​มี​เหตุผล​รองรับ​จริง ไม่ใช่​เพราะ “เห็น​คน​อื่น​ทำ”