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 classpublic 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 เดิม เพราะไม่มีใครรู้ว่าตรรกะนี้มีสำเนาอยู่ที่อื่นอีก จนกว่าจะมีลูกค้าร้องเรียนว่าใบแจ้งหนี้กับใบสั่งซื้อคำนวณส่วนลดไม่ตรงกัน
ทางแก้และการ refactor
หัวข้อที่มีชื่อว่า “ทางแก้และการ refactor”หลักการคือดึงตรรกะที่ซ้ำออกมาไว้ที่เดียว แล้วให้ทุกจุดที่ต้องการใช้เรียกผ่านจุดนั้น — ตาม 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:
- ระบุจุดที่คัดลอก — ใช้เครื่องมือตรวจจับ code ซ้ำ (duplicate code detector) หรือสังเกตด้วยตาเมื่อรีวิว code
- ดึง abstraction ที่เหมาะสม — method กลางถ้าซ้ำใน class เดียว, superclass หรือ interface ถ้าซ้ำข้าม class, หรือ Strategy object อย่างตัวอย่างข้างต้นถ้าพฤติกรรมอาจแตกต่างกันตามบริบท
- แทนที่ทุกสำเนาให้เรียกใช้จุดกลาง — อย่าเหลือสำเนาเก่าไว้ “เผื่อพัง” เพราะมันจะกลับมาสร้างปัญหาเดิมซ้ำ
- คุมด้วยการรีวิว code และ static analysis — ป้องกันไม่ให้ทีมกลับไปคัดลอกวางซ้ำในอนาคต ผ่าน Boy Scout Rule และการทำ Refactoring อย่างสม่ำเสมอ
ทั้งนี้ SourceMaking และ Wikipedia ก็ยอมรับว่าการคัดลอกวางไม่ใช่บาปเสมอไป — boilerplate, code snippet มาตรฐาน, หรือการ unroll loop ในบางภาษาที่ compiler ไม่รองรับ ยังถือว่ายอมรับได้ เพราะไม่ใช่ “ความรู้ทางธุรกิจ” ที่จะเปลี่ยนแปลงพร้อมกันในอนาคต สิ่งที่อันตรายคือการคัดลอกตรรกะทางธุรกิจที่มีแนวโน้มจะต้องแก้ไขพร้อมกันทุกจุด