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

Rubber Duck Debugging

อธิบาย​ปัญหา​ให้​เป็ด​ยาง​ฟัง แล้ว​คำ​ตอบ​มัก​ปรากฏ​ขึ้น​เอง

Rubber duck debugging (บาง​ครั้ง​เรียก​สั้น ๆ ว่า “rubber ducking”) คือ​แนว​ปฏิบัติ​ที่​โปรแกรมเมอร์​อธิบาย code ของ​ตน​ออก​มา​เป็น​ภาษา​พูด​ที​ละ​บรรทัด ให้​กับ​วัตถุ​ไร้​ชีวิต เช่น ของเล่น​เป็ด​ยาง​ตัว​เล็ก ๆ ที่​วาง​อยู่​บน​โต๊ะ​ทำงาน โดย​ไม่​คาด​หวัง​คำ​ตอบ​ใด ๆ จาก​มัน ชื่อ​ของ​เทคนิค​นี้​มา​จาก​เกร็ด​เรื่อง​เล่า​ใน​หนังสือ The Pragmatic Programmer ของ Andy Hunt และ Dave Thomas ที่​เล่า​ถึง​ผู้​ช่วย​วิจัย​คน​หนึ่ง​ชื่อ Greg Pugh ซึ่ง​พก​เป็ด​ยาง​ติดตัว​ไว้​อธิบาย​ปัญหา code ให้​ฟัง​อยู่​เสมอ หนังสือ​บรรยาย​ไว้​ว่า​คู่​สนทนา “ไม่​จำเป็น​ต้อง​พูด​สัก​คำ การ​อธิบาย​ที​ละ​ขั้นตอน​ว่า code ควร​ทำ​อะไร มัก​ทำให้​ปัญหา​กระโดด​ออก​มา​จาก​หน้า​จอ​และ​ประกาศ​ตัว​มัน​เอง​ออก​มา”

หัวใจ​สำคัญ​คือ เป็ด​ไม่​ได้​เป็น​ผู้​แก้ bug — คุณ​ต่างหาก​ที่​แก้ bug ได้​ด้วย​การ​พูด​อธิบาย​สิ่ง​ที่ code ทำ​ออก​มา​ดัง ๆ ที​ละ​บรรทัด เป็ด​เป็น​เพียง​ข้อ​อ้าง​ทาง​สังคม​ที่​ทำให้การ​พูด​คน​เดียว​ไม่รู้สึก​ประหลาด ตัว​เป็ด​เอง​ไม่ใช่​สิ่ง​จำเป็น จะ​เป็น​ตุ๊กตา​หมี เพื่อน​ร่วม​งาน​ที่​ไม่มี​ความ​รู้เรื่อง code เลย หรือ​แม้แต่​การ​เขียน​อธิบาย​ปัญหา​ลง​ใน​อีเมล​ที่​ไม่​เคย​ส่ง​ออก​ไป​จริง ก็​ให้​ผลลัพธ์​แบบ​เดียวกัน​ได้

เบื้องหลัง​ปรากฏการณ์​นี้​อธิบาย​ได้​ด้วย​แนวคิด​ทาง​จิตวิทยาการ​เรียนรู้​ที่​เรียก​ว่า self-explanation effect และ metacognition (การ​คิด​เกี่ยว​กับ​กระบวนการ​คิด​ของ​ตนเอง) เมื่อ​เรา​พูด​อธิบาย​ออก​มา​ดัง ๆ สมอง​จะ​เปลี่ยน​โหมด​การ​ประมวล​ผล​จาก “การ​อ่าน​แบบ​สแกน​หาความ​คุ้น​เคย” (recognition) ไป​เป็น “การ​สร้าง​คำ​อธิบาย​ขึ้น​ใหม่​ที​ละ​ขั้น” (generation) การ​เปลี่ยน​โหมด​นี้​เอง​ที่​บังคับ​ให้​สมมติฐาน​ที่​ซ่อน​อยู่​ใน​หัว​ถูก​พูด​ออก​มา​เป็น​คำ​พูด​ที่​ตรวจสอบ​ได้ ช่องว่าง​ระหว่าง “สิ่ง​ที่​คิด​ว่า code ทำ” กับ “สิ่ง​ที่ code ทำ​จริง” จึง​มัก​โผล่​ออก​มา​ให้​เห็น​ตรง​จุด​นั้น​เอง

เทคนิค​นี้​ไม่​จำเป็น​ต้อง “พูด” เสมอ​ไป การ เขียน อธิบาย​ปัญหา​ก็ได้​ผลลัพธ์​แบบ​เดียวกัน หลาย​ทีม​เรียก​ปรากฏการณ์​นี้​ว่า “Stack Overflow effect” — โปรแกรมเมอร์​นั่ง​พิมพ์​คำถาม​อธิบาย​ปัญหา​อย่าง​ละเอียด​เพื่อ post ถาม​ใน community แต่​พอ​พิมพ์​ไป​ได้​ครึ่ง​ทาง กลับ​พบ​คำ​ตอบ​ด้วย​ตัวเอง​เสีย​ก่อน จน​บาง​ครั้ง​ไม่ทัน​ได้​กด post เลย​ด้วย​ซ้ำ กลไก​เบื้องหลัง​เหมือน​กัน​ทุก​ประการ คือ​การ​บังคับ​ตัวเอง​ให้​อธิบาย​อย่าง​มี​โครงสร้าง​ให้​ผู้​อ่าน/ผู้​ฟัง​ที่​ไม่รู้​บริบท​มา​ก่อน​เข้าใจ​ได้

  1. หา​ตัว​ฟัง วาง​เป็ด​ยาง ตุ๊กตา หรือ​ของเล่น​ชิ้น​ใด​ก็ได้​ไว้​ใกล้​จอ หรือ​เปิด​เอกสาร​เปล่า​ไว้​พิมพ์​อธิบาย​ก็ได้​ถ้า​ไม่มี​ของเล่น
  2. บอก​เป้าหมาย​ก่อน อธิบาย​ให้​เป็ด​ฟัง​ว่า code ส่วน​นี้ ควร​จะ ทำ​อะไร ไม่ใช่​แค่​สิ่ง​ที่​มัน​ทำ​อยู่​ตอน​นี้ — การ​พูด​เจตนา​ออก​มา​ก่อน​คือ​กุญแจ​ที่​ทำให้​เห็น gap
  3. อ่าน code ที​ละ​บรรทัด​ออก​เสียง บรรยาย​ทุก​ขั้นตอน​อย่าง​ละเอียด แม้​บรรทัด​ที่​ดูเหมือน “ชัดเจน​อยู่​แล้ว​ไม่​ต้อง​พูด​ก็ได้” ก็​ควร​พูด​ออก​มา เพราะ​จุด​ที่​มั่นใจ​เกิน​จน​ข้าม​อธิบาย​มัก​เป็น​จุด​ที่ bug ซ่อน​อยู่
  4. ฟัง​ตัวเอง สังเกต​ช่วง​ที่​คำ​อธิบาย​เริ่ม​ติดขัด อธิบาย​ไม่​ลื่น​ไหล หรือ​พบ​ว่า​ประโยค​ที่​พูด​ขัดแย้ง​กับ code ที่​เห็น​ตรง​หน้า — จุด​เหล่า​นี้​คือ​สัญญาณ​เตือน
  5. ถ้า​ยัง​ไม่​เจอ ค่อย​ยก​ระดับ เมื่อ​อธิบาย​จน​จบ​แล้ว​ยัง​ไม่​พบ​ทางออก ค่อย​นำ​ปัญหา​ไป​ถาม​เพื่อน​ร่วม​งาน​หรือ​ใช้ debugger เชิง​ลึก​ต่อ — rubber duck debugging เป็น​ด่าน​แรก​ที่​ถูก​และ​เร็ว ไม่ใช่​ตัว​ทดแทน debugger ถาวร
flowchart TD
    Start[เจอ bug ที่แก้ไม่ตก]
    Explain[อธิบายเป้าหมายและ code ทีละบรรทัดให้เป็ดฟัง]
    Gap{พบช่องว่างระหว่างที่คิดกับที่ code ทำจริงไหม}
    Fix[แก้ bug ตามที่ค้นพบ]
    Ask[ไปถามเพื่อนร่วมงานหรือใช้ debugger ต่อ]
    Start --> Explain --> Gap
    Gap -->|พบ| Fix
    Gap -->|ไม่พบ| Ask

สมมติ​มี method ที่​ควร​กรอง​เฉพาะ​ออเดอร์​ที่​ยัง​ไม่​ชำระ​เงิน แต่​กลับ​คืน​ค่า​ออเดอร์​ที่​ชำระ​แล้ว​ปน​มา​ด้วย ลอง​พูด​อธิบาย​ให้​เป็ด​ฟัง​ที​ละ​บรรทัด:

public IEnumerable<Order> GetUnpaidOrders(IEnumerable<Order> orders)
{
// "เป้าหมายของ method นี้คือคืนเฉพาะออเดอร์ที่ IsPaid เป็น false นะเป็ด"
return orders.Where(o => o.IsPaid = false);
// พอพูดถึงบรรทัดนี้ออกเสียงว่า "IsPaid เท่ากับ false"
// จะสะดุดทันทีว่านี่คือเครื่องหมาย = ตัวเดียว (assignment)
// ไม่ใช่ == (การเปรียบเทียบ) จึงเป็นการเซ็ตค่า IsPaid ให้เป็น false
// ให้ทุกออเดอร์ แล้วเงื่อนไขก็เป็น false เสมอ (เพราะค่าที่ assign กลับมาคือ false)
// ผลลัพธ์คือ Where() กรองทุกแถวทิ้งหมด ไม่ใช่ bug ที่คิดไว้ตอนแรกเลย
}

การ​พูด​ออก​เสียง​คำ​ว่า “เท่ากับ” ตรง​บรรทัด o.IsPaid = false มัก​ทำให้​ผู้​เขียน​สะดุด​กับ​ความ​กำกวม​ของ = กับ == ได้​เร็ว​กว่า​การ​อ่าน​ด้วย​ตา​เงียบ ๆ เพราะ​การ​อ่าน​ด้วย​ตา​อย่าง​เดียว​สมอง​มัก​จะ “สแกน​แบบ​คุ้น​เคย” และ​มอง​ข้าม​รูปแบบ​ที่​เคยชิน​ไป​โดย​ไม่ทัน​สังเกต

อีก​ตัวอย่าง​ที่​พบ​บ่อย​คือ​การ​อธิบาย configuration หรือ pipeline ที่​ซับซ้อน เช่น middleware order ใน ASP.NET Core:

var app = builder.Build();
// "อธิบายให้เป็ดฟังว่าลำดับ middleware นี้ต้องเป็นแบบนี้เพราะอะไร"
app.UseAuthentication(); // ต้องรู้ตัวตนผู้ใช้ก่อน
app.UseAuthorization(); // ถึงจะเช็คสิทธิ์ได้
app.MapControllers();
// พอพูดถึงบรรทัดที่ 2 ออกเสียงว่า "UseAuthorization ต้องมาหลัง UseAuthentication เสมอ"
// มักทำให้เห็นว่าจริง ๆ แล้ว project นี้สลับลำดับสองบรรทัดนี้ผิดมาตั้งแต่ต้น
// ทำให้ endpoint ที่ควรถูก block กลับเข้าถึงได้โดยไม่ต้องมี token

ประโยชน์

  • ไม่​ต้อง​รบกวน​เพื่อน​ร่วม​งาน แก้​ปัญหา​ได้​ทันที​โดย​ไม่​ต้อง​รอ​คิว​ใคร​ว่าง
  • บังคับ​ให้​ช้า​ลง​และ​อธิบาย​ที​ละ​ขั้น ซึ่ง​มัก​เผย​สมมติฐาน​ที่​ไม่​เคย​พูด​ออก​มา​ดัง ๆ มา​ก่อน
  • ฝึก​ทักษะ​การ​สื่อสาร​ทาง​เทคนิค เพราะ​การ​อธิบาย code ให้​คน​ที่​ไม่รู้เรื่อง​เลย​ฟัง​ต้อง​ใช้​ภาษา​ที่​ชัดเจน​ขึ้น
  • ใช้ได้​ทุก​ที่ ไม่มี​ต้นทุน​เครื่องมือ และ​เป็น​ด่าน​คัด​กรอง​ก่อน​ไป​รบกวน​คน​อื่น​ด้วย​คำถาม​ที่​ตัวเอง​ยัง​ตอบ​ได้​อยู่​แล้ว
  • อาจารย์​สอน​วิทยาการ​คอมพิวเตอร์​หลาย​คน เช่น​ใน​วิชา CS50 ที่ Harvard นำ​เทคนิค​นี้​ไป​บรรจุ​เป็น​ส่วน​หนึ่ง​ของ​กระบวนการ​สอน debugging อย่าง​เป็น​ทางการ

ข้อ​ควร​ระวัง

  • ได้​ผล​ดี​กับ bug ที่​เกิด​จาก​ความ​เข้าใจ​ผิด​ของ​ผู้​เขียน​เอง (logic error, off-by-one, เงื่อนไข​ผิด) แต่​ไม่​ช่วย​อะไร​กับ bug ที่​ต้อง​อาศัย​ข้อมูล​ภายนอก เช่น ปัญหา infrastructure, race condition ที่​สุ่ม​เกิด หรือ​พฤติกรรม​ของ third-party library ที่​ต้อง​พิสูจน์​ด้วย​การ​รัน debugger จริง
  • ถ้า​อธิบาย​แบบ​ข้าม ๆ หรือ​พูด​เฉพาะ​บรรทัด​ที่ “มั่นใจ​ว่า​ไม่มี​ปัญหา” เทคนิค​นี้​จะ​ไม่​ได้​ผล ต้อง​อธิบาย​ทุก​บรรทัด​อย่าง​ละเอียด​จริง ๆ รวม​ถึง​บรรทัด​ที่​ดู​ธรรมดา​ที่สุด
  • บาง​คน (รวม​ถึง​ผู้​เขียน​หลาย​คนใน community) พบ​ว่า​เทคนิค​นี้​ไม่​ได้​ผล​กับ​ทุก​คน​หรือ​ทุก​สถานการณ์​เท่า​กัน — มัน​เป็น​เครื่อง​มือหนึ่ง​ใน​กล่อง​เครื่องมือ ไม่ใช่​ยาครอบจักรวาล
  • ใน​ยุค​ที่​มี AI pair programmer อย่าง GitHub Copilot บาง​ทีม​เริ่ม​ใช้ AI เป็น “เป็ด​ยาง​ที่​ตอบ​กลับ​ได้” แทน ซึ่ง​มี​ข้อดี​คือ​ได้ feedback จริง แต่​ก็​เสี่ยง​ข้าม​ขั้นตอน​การ​อธิบาย​ด้วย​คำ​พูด​ของ​ตัวเอง​ไป​เลย ทำให้​เสีย​ประโยชน์​ด้าน self-explanation effect ที่​เป็น​แก่น​ของ​เทคนิค​นี้
  • Pair Programming — ถ้า​อธิบาย​ให้​เป็ด​ฟัง​แล้ว​ยัง​ไม่​เจอ ขั้น​ถัด​ไป​คือ​หา​คนจริง​มา​ฟัง​ด้วย
  • Test-Driven Development — การ​เขียน​เทสก่อนก็​บังคับ​ให้​อธิบาย​พฤติกรรม​ที่​คาด​หวัง​ออก​มา​เป็น code เช่น​เดียวกัน
  • Readable Code — code ที่​อธิบาย​ตัวเอง​ได้​ง่าย​เมื่อ​พูด​ออก​เสียง มัก​เป็น code ที่​อ่าน​ง่าย​สำหรับ​คน​อื่น​ด้วย
  • Refactor Mercilessly — จุด​ที่​อธิบาย​ให้​เป็ด​ฟัง​แล้ว​ติดขัด​บ่อย ๆ มัก​เป็น​สัญญาณ​ว่า​ถึง​เวลา​ต้อง refactor
  • Naming — ถ้า​อธิบาย code ให้​เป็ด​ฟัง​แล้ว​ต้อง​ใช้​ประโยค​ยาว ๆ อธิบาย​ชื่อ ตัวแปร​นั้น​อาจ​ตั้ง​ชื่อ​ได้​ดี​กว่า​นี้