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

Copy-Paste Programming

คัด​ลอก code ไป​วาง​ซ้ำ ๆ แทนที่​จะ​รวบ​ไว้​ที่​เดียว

Copy-Paste Programming คือ​การนำ code ที่​มี​อยู่​แล้วไป​คัด​ลอก​วาง​ใน​ตำแหน่ง​ใหม่ แล้ว​แก้ไข​เล็กน้อย​ให้​เข้า​กับ​บริบท​ใหม่ แทนที่​จะ​ดึง​ตรรกะ​ที่​ใช้​ร่วม​กัน​ออก​มา​เป็น method class หรือ module กลาง​แล้ว​เรียก​ใช้​จาก​หลาย​จุด ผลลัพธ์​คือ codebase ที่​เต็ม​ไป​ด้วย “code ฝาแฝด” — ท่อน code ที่​คล้าย​กัน​มาก​แต่​ไม่​ได้​เชื่อม​โยง​กัน​ใน​ระบบ​เลย เมื่อใด​ก็ตาม​ที่​ต้อง​แก้​กติกา​ทาง​ธุรกิจ ทีม​ต้อง​ไล่​ตามหา​ทุก​จุด​ที่​เคย​คัด​ลอก​ไป​วาง ซึ่ง​มัก​ตาม​ไม่​ครบ

Wikipedia นิยาม​ว่า​นี่​คือ “การ​ผลิต code ที่​ซ้ำซ้อน​สูง อัน​เกิด​จาก​การ​คัด​ลอก​และ​วาง” และ​มัก​ใช้​ใน​เชิง​ลบ​เพื่อ​สื่อ​ว่า​โปรแกรมเมอร์​ขาด​ความ​สามารถ​ใน​การ​สร้าง abstraction ส่วน SourceMaking อธิบาย​ว่า​นี่​คือ​รูปแบบ​หนึ่ง​ของ​การ “reuse ที่​เสื่อม​สภาพ” — มัน​มา​จาก​สัญชาตญาณ​ที่​ถูกต้อง (แก้​ของ​เดิม​ง่าย​กว่า​เขียน​ใหม่) แต่​ถูก​ทำ​อย่าง​ผิด​วิธี

  • เร็ว​ใน​ระยะ​สั้น — คัด​ลอก​วาง​แล้ว​ปรับ​นิดหน่อย​เสร็จ​เร็ว​กว่า​คิด abstraction ใหม่​ทั้งหมด โดย​เฉพาะ​เมื่อ deadline บีบ
  • ไม่​ต้อง​เข้าใจ code เดิม​ลึกซึ้ง — ไม่​ต้อง​วิเคราะห์​ว่า​จะ​ออกแบบ interface หรือ base class อย่างไร​ให้​ครอบคลุม​ทุก​กรณี​การ​ใช้งาน แค่ก็อปมา​แล้ว​แก้​ตรง​ที่​ต่าง​กัน
  • ดู​ปลอดภัย​กว่า​ใน​สายตา​คน​ไม่​ชำนาญ — เพราะ​ไม่​แตะ code เดิม​เลย จึง​รู้สึก​ว่า “ไม่​เสี่ยง​ทำ​ของ​เดิม​พัง” ทั้ง​ที่​จริง​กลับ​สร้าง​ความ​เสี่ยง​สะสม​ใน​ระยะ​ยาว
  • นับ​บรรทัด code ได้​เยอะ — ใน​ทีม​ที่​ยัง​วัดผล​งาน​ด้วย​จำนวน​บรรทัด code (ผิด​วิธี) การ​คัด​ลอก​วาง​ทำให้​ตัวเลข​นี้​ดู​ดี​ขึ้น​โดย​ไม่​ต้อง​คิด​อะไร​มาก
  • ละเมิด Don’t Repeat Yourself โดยตรง — ความ​รู้​หนึ่ง​อย่าง (กติกา​คำนวณ​ส่วนลด, การ​ตรวจสอบ​ข้อมูล ฯลฯ) ควร​มี​ตัวแทน​เพียง​จุด​เดียว​ใน​ระบบ ไม่ใช่​กระจาย​เป็น​สิบ​จุด
  • bug ถูก​ทวีคูณ — ถ้า code ต้นฉบับ​มี​ข้อ​ผิดพลาด ทุก​สำเนา​ก็​พก​ข้อ​ผิดพลาด​นั้น​ไป​ด้วย และ​การ​แก้ไข​ต้อง​ตาม​แก้​ที​ละ​จุด ซึ่ง​มัก​ตกหล่น​อย่าง​น้อยหนึ่ง​แห่ง​เสมอ
  • แก้ไข​ไม่​ครบ = พฤติกรรม​ไม่​สอดคล้อง​กัน — จุด​หนึ่ง​อัปเดต​กติกา​ใหม่​แล้ว อีก​จุด​ยัง​ใช้​กติกา​เก่า ผู้​ใช้​เจอ​พฤติกรรม​ระบบ​ที่​ขัดแย้ง​กัน​โดย​ไม่มี​เหตุผล​ทาง​ธุรกิจ​รองรับ
  • นำ​ไป​สู่ Spaghetti Code — ยิ่ง​คัด​ลอก​มาก โครงสร้าง​ยิ่ง​พัน​กัน เพราะ​แต่ละ​สำเนา​ถูก​แก้ไข​แยก​กัน​ไป​เรื่อย ๆ จน​ไม่มี​จุดศูนย์กลาง​ของ​ตรรกะ​เหลือ​อยู่​เลย
  • code รีวิว​และ​ทดสอบ​หนัก​ขึ้น​โดย​เปล่า​ประโยชน์ — ต้อง​อ่าน​และ​ทดสอบ code ที่​แทบ​จะ​เหมือน​กัน​ซ้ำ​หลาย​รอบ โดย​ไม่​ได้​เพิ่ม​คุณค่า​ทาง​ธุรกิจ​ใด ๆ
  • technical debt สะสม​เงียบ ๆ — ต้นทุน​ของ​การ​คัด​ลอก​วาง​ไม่​ปรากฏ​ทันที แต่​จะ​โผล่​มา​ตอน​ที่​ต้อง​เปลี่ยน​กติกา​ทาง​ธุรกิจ ซึ่ง​เป็น​ช่วง​ที่​ทีม​มัก​ประเมิน​ผลกระทบ​ผิดพลาด เพราะ​ไม่รู้​ว่า​มี​สำเนา​ซ่อน​อยู่​กี่​จุด

แผนภาพ​ด้าน​ล่าง​แสดง​ให้​เห็น​ว่าการ​เปลี่ยน​กติกา​หนึ่ง​อย่าง​ที่​ถูก​คัด​ลอก​ไป​สาม​ที่ มัก​จบ​ลง​ด้วย​การ​แก้ไข​ที่​ไม่​ครบ​ทุก​จุด:

flowchart LR
  Change[ต้องแก้กติกาคำนวณส่วนลด]
  Copy1[OrderService]
  Copy2[InvoiceService]
  Copy3[ReportExporter]
  Change --> Copy1
  Change --> Copy2
  Change --> Copy3
  Copy1 --> Fixed[แก้ไขแล้วถูกต้อง]
  Copy2 --> Missed1[ลืมแก้ ยังใช้กติกาเก่า]
  Copy3 --> Missed2[ลืมแก้ ยังใช้กติกาเก่า]

code 2 class ด้าน​ล่าง​คำนวณ​ส่วนลด​ด้วย​ตรรกะ​เดียวกัน แต่​ถูก​คัด​ลอก​วาง​แยก​กัน เมื่อ​ธุรกิจ​เปลี่ยน​อัตราส่วนลด​สำหรับ​ยอด​ซื้อ​จำนวน​มาก​จาก 5% เป็น 7% ทีม​แก้​เฉพาะ OrderService แล้ว​ลืม InvoiceService ไป:

// ตัวอย่าง Copy-Paste Programming: ตรรกะคำนวณส่วนลดถูกคัดลอกไป2 class
public class OrderService
{
public decimal CalculateDiscount(Order order)
{
decimal discount = 0m;
if (order.Customer.IsVip)
{
discount = order.Total * 0.15m;
}
else if (order.Total > 1000m)
{
discount = order.Total * 0.07m; // อัปเดตแล้วตามนโยบายใหม่
}
return discount;
}
}
public class InvoiceService
{
// คัดลอกมาจาก OrderService แล้วแก้แค่ชื่อ parameter
public decimal CalculateInvoiceDiscount(Invoice invoice)
{
decimal discount = 0m;
if (invoice.Customer.IsVip)
{
discount = invoice.Total * 0.15m;
}
else if (invoice.Total > 1000m)
{
discount = invoice.Total * 0.05m; // ลืมอัปเดตเป็น 0.07m
}
return discount;
}
}

สังเกต​ว่า InvoiceService ยัง​ใช้​อัตรา 0.05m เดิม เพราะ​ไม่มี​ใคร​รู้​ว่า​ตรรกะ​นี้​มี​สำเนา​อยู่​ที่​อื่น​อีก จนกว่า​จะ​มี​ลูกค้า​ร้องเรียน​ว่า​ใบ​แจ้ง​หนี้​กับ​ใบสั่ง​ซื้อ​คำนวณ​ส่วนลด​ไม่​ตรง​กัน

หลักการ​คือ​ดึง​ตรรกะ​ที่​ซ้ำ​ออก​มา​ไว้​ที่​เดียว แล้ว​ให้​ทุก​จุด​ที่​ต้องการ​ใช้​เรียก​ผ่าน​จุด​นั้น — ตาม Once and Only Once refactoring.guru เรียก code ซ้ำ​แบบ​นี้​ว่า Duplicate Code smell และ​แนะนำ​ให้​ใช้ Extract Method ดึง code ร่วม​ออก​มา​เป็น method เดียว หาก​ซ้ำ​ข้าม class ให้​ใช้ Extract Class หรือ Extract Superclass เพื่อ​รวม​ศูนย์​ตรรกะ​ไว้​ใน​ที่​เดียว:

// รวบตรรกะคำนวณส่วนลดไว้ที่เดียว แล้วให้ทุกจุดเรียกผ่าน policy เดียวกัน
public interface IDiscountPolicy
{
decimal Calculate(ICustomer customer, decimal total);
}
public class StandardDiscountPolicy : IDiscountPolicy
{
private const decimal VipRate = 0.15m;
private const decimal BulkRate = 0.07m;
private const decimal BulkThreshold = 1000m;
public decimal Calculate(ICustomer customer, decimal total)
{
if (customer.IsVip)
{
return total * VipRate;
}
return total > BulkThreshold ? total * BulkRate : 0m;
}
}
public class OrderService
{
private readonly IDiscountPolicy _discountPolicy;
public OrderService(IDiscountPolicy discountPolicy) => _discountPolicy = discountPolicy;
public decimal CalculateDiscount(Order order) =>
_discountPolicy.Calculate(order.Customer, order.Total);
}
public class InvoiceService
{
private readonly IDiscountPolicy _discountPolicy;
public InvoiceService(IDiscountPolicy discountPolicy) => _discountPolicy = discountPolicy;
public decimal CalculateDiscount(Invoice invoice) =>
_discountPolicy.Calculate(invoice.Customer, invoice.Total);
}

ตอน​นี้​เมื่อ​ธุรกิจ​เปลี่ยน​อัตราส่วนลด​อีก​ครั้ง มี​จุด​เดียว​ที่​ต้อง​แก้ — StandardDiscountPolicy — และ​ทั้ง OrderService กับ InvoiceService จะ​ได้​ค่าที่​ถูกต้อง​เสมอ​โดย​อัตโนมัติ

แนวทาง​ปฏิบัติ​เมื่อ​เจอ Copy-Paste Programming ใน codebase:

  1. ระบุ​จุด​ที่​คัด​ลอก — ใช้​เครื่องมือ​ตรวจ​จับ code ซ้ำ (duplicate code detector) หรือ​สังเกต​ด้วย​ตา​เมื่อ​รีวิว code
  2. ดึง abstraction ที่​เหมาะสม — method กลาง​ถ้า​ซ้ำ​ใน class เดียว, superclass หรือ interface ถ้า​ซ้ำ​ข้าม class, หรือ Strategy object อย่าง​ตัวอย่าง​ข้าง​ต้น​ถ้า​พฤติกรรม​อาจ​แตก​ต่าง​กัน​ตาม​บริบท
  3. แทนที่​ทุก​สำเนา​ให้​เรียก​ใช้​จุด​กลาง — อย่า​เหลือ​สำเนา​เก่า​ไว้ “เผื่อ​พัง” เพราะ​มัน​จะ​กลับ​มาสร้าง​ปัญหา​เดิม​ซ้ำ
  4. คุม​ด้วย​การ​รีวิว code และ static analysis — ป้องกัน​ไม่​ให้​ทีม​กลับ​ไป​คัด​ลอก​วาง​ซ้ำ​ใน​อนาคต ผ่าน Boy Scout Rule และ​การ​ทำ Refactoring อย่าง​สม่ำเสมอ

ทั้งนี้ SourceMaking และ Wikipedia ก็​ยอมรับ​ว่าการ​คัด​ลอก​วาง​ไม่ใช่​บาป​เสมอ​ไป — boilerplate, code snippet มาตรฐาน, หรือ​การ unroll loop ใน​บาง​ภาษา​ที่ compiler ไม่​รองรับ ยัง​ถือว่า​ยอมรับ​ได้ เพราะ​ไม่ใช่ “ความ​รู้​ทาง​ธุรกิจ” ที่​จะ​เปลี่ยนแปลง​พร้อม​กัน​ใน​อนาคต สิ่ง​ที่​อันตราย​คือ​การ​คัด​ลอกตรรกะ​ทาง​ธุรกิจที่​มี​แนวโน้ม​จะ​ต้อง​แก้ไข​พร้อม​กัน​ทุก​จุด