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

Don't Repeat Yourself

ความ​ซ้ำซ้อน​คือ​ความ​สูญเปล่า

หลักการ Don’t Repeat Yourself (DRY) กล่าว​ว่า ความ​ซ้ำซ้อน​ใน​ตรรกะ (logic) ควร​ถูก​กำจัด​ด้วย abstraction ส่วน​ความ​ซ้ำซ้อน​ใน​กระบวนการ (process) ควร​ถูก​กำจัด​ด้วย​การ​ทำงาน​อัตโนมัติ (automation) — ความ​ซ้ำซ้อน​คือ​ความ​สูญเปล่า (Duplication is Waste)

DRY ถูก​บัญญัติ​ขึ้น​โดย Andy Hunt และ Dave Thomas ใน​หนังสือ The Pragmatic Programmer (1999) โดย​ให้​นิยาม​ไว้​อย่าง​แม่นยำ​ว่า

“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system” (ความ​รู้​ทุก​ชิ้น​ต้อง​มี​ตัวแทน​เพียง​หนึ่ง​เดียว ที่​ชัดเจน และ​เป็น​แหล่ง​อ้างอิง​หลัก ภายใน​ระบบ)

จุด​ที่​มัก​เข้าใจ​ผิด​คือ​คิด​ว่า DRY หมาย​ถึง “code ต้อง​ไม่​ซ้ำ​กัน” เท่านั้น แต่​ตัว​นิยาม​จริง​พูด​ถึง ความ​รู้ (knowledge) ไม่ใช่​แค่ code (code) code สอง​ท่อน​อาจ​มี​รูป​หน้าตา​เหมือน​กัน​โดย​บังเอิญ (accidental duplication) โดย​ไม่​ได้​แทน “ความ​รู้” เดียวกัน​เลย​ก็ได้ — เช่น การ​ตรวจสอบ​ว่า​อายุ​มากกว่า 18 กับ​การ​ตรวจสอบ​ว่า​จำนวน​สินค้า​ใน​ตะกร้า​มากกว่า 18 ชิ้น อาจ​เขียน​เป็น if (x > 18) เหมือน​กัน แต่​เป็น​กฎ​ทาง​ธุรกิจ​คนละ​เรื่อง หาก​รีบ​ไป​รวม​เป็น abstraction เดียว วัน​หนึ่ง​กฎ​หนึ่ง​เปลี่ยน​ก็​จะ​ไป​กระทบ​อีก​กฎ​หนึ่ง​โดย​ไม่​ตั้งใจ นี่​คือ​ความ​แตก​ต่าง​ที่​สำคัญ​ระหว่าง duplication of knowledge (ควร​กำจัด) กับ incidental/coincidental duplication (code หน้าตา​คล้าย​กัน​แต่​คนละ​เหตุผล ซึ่ง​บาง​ครั้ง​ควร​ปล่อย​ไว้)

DRY ครอบคลุม​กว้าง​กว่า source code — มัน​ใช้ได้​กับ database schema (จึง​เชื่อม​โยง​กับ database normalization), เอกสารประกอบ, แผนการ​ทดสอบ, file build/config และ​แม้แต่​กระบวนการ​ทำงาน​ของ​ทีม เมื่อ​ระบบ​ยึด DRY ได้​จริง การ​แก้ไข​องค์​ประกอบ​หนึ่ง​จะ​ไม่​ต้อง​ไป​แก้​องค์​ประกอบ​อื่น​ที่​ไม่​เกี่ยวข้อง​กัน​ใน​เชิง​ตรรกะ ใน​ขณะ​ที่​องค์​ประกอบ​ที่​เกี่ยวข้อง​กัน​จะ​เปลี่ยน​พร้อม​กัน​เสมอ

Martin Fowler และ Kent Beck ใน​หนังสือ Refactoring จัด​ให้ Duplicated Code เป็น code smell ลำดับ​แรก​สุด​ที่​กล่าว​ถึง เพราะ​ความ​ซ้ำซ้อน​มัก​เป็น​สัญญาณ​เริ่มต้น​ที่​นำ​ไป​สู่​ปัญหา​การ​ดูแล​รักษา​อื่น ๆ ตาม​มา ส่วน​แนวทาง​เชิง​ปฏิบัติ​ที่​ช่วย​ไม่​ให้​รีบ abstraction เร็ว​เกิน​ไป​คือ Rule of Three (เชื่อ​กัน​ว่า​มา​จาก Don Roberts) — code คล้าย​กัน​สอง​แห่ง​ยัง​ไม่​จำเป็น​ต้อง refactor แต่​พอ​ซ้ำ​ถึง​ครั้ง​ที่​สาม​จึง​ค่อย​ดึง​ออก​มา​เป็น abstraction เพราะ​การ refactor ก่อน​เวลา​อัน​ควร​มี​ความ​เสี่ยง​ที่​จะ​เลือก abstraction ผิด​รูปแบบ ซึ่ง​แก้​ยาก​กว่า​ความ​ซ้ำซ้อน​เสีย​อีก​เมื่อ requirement เปลี่ยน​ไป

บ่อย​ครั้ง​ที่​คำ​สั่ง if-then และ switch มัก​ถูก​ทำซ้ำ​ใน​หลาย​ที่​ภายใน application เป็น​เรื่อง​ปกติ​ใน application ที่​มี​ระบบ​ความ​ปลอดภัย​ที่​จะ​มี function ต่าง​กัน​ให้​ผู้​ใช้​ใน​บทบาท (role) ต่าง​กัน code จึง​อาจ​เกลื่อน​ไป​ด้วย​การ​เช็ก “ผู้​ใช้​อยู่​ใน​บทบาท​นี้​หรือ​ไม่” application อื่น​อาจ​ถูก​ขยาย​ให้​รองรับ​โครงสร้าง​ข้อมูล​ที่​คล้าย​แต่​ต่าง​กัน​หลาย​แบบ โดย​มี switch อยู่​ทุก​ระดับ​ของ workflow เพื่อ​บรรยาย​พฤติกรรม​ที่​ต่าง​กัน ทุก​ที่​ที่​ทำได้ ควร refactor เงื่อนไข​เหล่า​นี้​ด้วย design pattern ที่​รู้จัก​กัน​ดี (เช่น Strategy หรือ Polymorphism) เพื่อ​ดึง​ความ​ซ้ำซ้อน​มา​รวม​ไว้​ที่​จุด​เดียว​ใน codebase

ฝั่ง​ตรง​ข้าม​ของ DRY ถูก​เรียก​เล่น ๆ ว่า WET (“Write Everything Twice” หรือ “We Enjoy Typing”) ซึ่ง​พบ​บ่อย​ใน architecture หลาย​ชั้น ที่​ต้อง​เขียน​สิ่ง​เดียวกัน​ซ้ำ​ใน​แต่ละ layer เช่น ชื่อ field “comment” ปรากฏ​ซ้ำ​ใน label, HTML, ชื่อ​ตัวแปร และ query ฐาน​ข้อมูล

ใน​ช่วง​หลัง มี​แนวคิด AHA (Avoid Hasty Abstractions) ที่ Sandi Metz และ​คน​อื่น ๆ ผลักดัน โดย​เตือน​ว่าการ​รีบ​สร้าง abstraction ก่อน​เห็น​รูปแบบ​ซ้ำ​ที่​ชัดเจน​พอ อาจ​ทำให้ code “แข็ง” (rigid) และ​ดูแล​ยาก​กว่า​ความ​ซ้ำซ้อน​ที่​ยัง​ไม่​ได้​แก้​เสีย​อีก แนวคิด​นี้​ไม่​ได้​ปฏิเสธ DRY แต่​เป็นการ​เตือน​ให้​ใช้ DRY อย่าง​มี​วิจารณญาณ ควบคู่​กับ Rule of Three — รอ​ให้​เห็น​รูปแบบ​ซ้ำ​จริง​ก่อน​ค่อย abstraction ไม่ใช่ abstraction ล่วงหน้า​เพื่อ​ป้องกัน​การ​ซ้ำ​ที่​ยัง​มา​ไม่​ถึง

การ​เพิ่ม code ที่​ไม่​จำเป็น​เข้าไป​ใน codebase ทำให้​ปริมาณ​งาน​ที่​ต้อง​ใช้​ใน​การ​ขยาย​และ​ดูแล​ซอฟต์แวร์​ใน​อนาคต​เพิ่ม​ขึ้น code ที่​ซ้ำซ้อน (duplicate code) เพิ่มพูน technical debt ไม่​ว่า​ความ​ซ้ำซ้อน​จะ​เกิด​จาก​การ Copy-Paste Programming หรือ​จาก​ความ​ไม่​เข้าใจ​ว่า​จะ​ใช้ abstraction อย่างไร มัน​ก็​ลด​คุณภาพ​ของ code ลง เหตุผล​หลัก​ที่​ทำให้​ความ​ซ้ำซ้อน​อันตราย ได้แก่

  • ต้นทุน​การ​แก้ bug คูณ​สอง (หรือ​มากกว่า): เมื่อ​พบ​ข้อ​ผิดพลาด​ใน​ตรรกะ​ที่​ถูกก็อปปี้​ไว้​หลาย​จุด นัก​พัฒนา​ต้อง​จำ​ให้​ได้​ว่า​มี​กี่​จุด และ​แก้​ให้​ครบ​ทุก​จุด พลาด​จุด​ใด​จุด​หนึ่ง​ไป​คือ bug ที่​ซ่อน​อยู่​ทันที
  • แหล่ง​ความ​จริง​กำกวม: เมื่อ​ไม่มี “single source of truth” ทีม​ต้อง​เดา​ว่า version ไหน​ถูกต้อง version ไหน​ล้าสมัย
  • ขนาด codebase บวม​โดย​ไม่​จำเป็น: ยิ่ง code เยอะ ยิ่ง​ใช้​เวลา​อ่าน ทำความ​เข้าใจ และ​ทดสอบ​มาก​ขึ้น โดย​ไม่​ได้​เพิ่ม​มูลค่า​ทาง​ธุรกิจ
  • ขัด​ขวาง​การ​เปลี่ยนแปลง: กฎ​ธุรกิจ​ที่​ควร​เปลี่ยน​ได้​จาก​จุด​เดียว กลับ​ต้อง​ไล่​ตาม​แก้​หลาย​ที่ ทำให้​ทีม​กลัว​การ​เปลี่ยนแปลง​และ agility ลดลง

ความ​ซ้ำซ้อน​ใน​กระบวนการ​ก็​เป็น​ความ​สูญเปล่า​เช่น​กัน​หาก​ทำให้​อัตโนมัติ​ได้ กระบวนการ​ทดสอบ การ build และ​การ integrate ด้วย​มือ ควร​ถูก​กำจัด​ทุก​ครั้ง​ที่​เป็น​ไป​ได้​ด้วย​การ​ใช้ automation (CI/CD, automated test suite) นี่​คือ​มิติ​ของ DRY ที่​มัก​ถูก​มอง​ข้าม — DRY ไม่ใช่​แค่​เรื่อง code แต่​เป็น​เรื่อง​ของ “อย่า​ทำงาน​ที่​เครื่องจักร​ทำ​แทน​ได้ ซ้ำ​ด้วย​มือ”

ตัวอย่าง​คลาสสิก​คือ business rule ที่​ถูก​กระจาย​ไป​คำนวณ​ซ้ำ​ใน​หลาย​จุด​ของ​ระบบ เมื่อ​กฎ​เปลี่ยน ต้อง​ไล่​แก้​ทุก​จุด และ​มี​ความ​เสี่ยง​สูง​ที่​จะ​แก้​ไม่​ครบ

// ก่อนแก้ (Bad): กฎ "ลูกค้า VIP" (ยอดซื้อสะสม > 100,000 บาท) ถูกคำนวณซ้ำในหลายที่
public class OrderService
{
public decimal CalculateDiscount(Customer customer, decimal orderTotal)
{
// กฎ VIP ถูกเขียนซ้ำที่นี่...
bool isVip = customer.TotalPurchases > 100_000m;
return isVip ? orderTotal * 0.10m : orderTotal * 0.02m;
}
}
public class ShippingService
{
public decimal CalculateShippingFee(Customer customer, decimal baseFee)
{
// ...และซ้ำอีกครั้งที่นี่ ด้วยตรรกะเดียวกันแต่พิมพ์ใหม่
bool isVip = customer.TotalPurchases > 100_000m;
return isVip ? 0m : baseFee;
}
}
public class NewsletterService
{
public string GetGreeting(Customer customer)
{
// ...และซ้ำอีกครั้งที่สาม จุดที่ค่า threshold อาจพิมพ์ผิดโดยไม่รู้ตัว
bool isVip = customer.TotalPurchases > 100_000m;
return isVip ? "เรียน ลูกค้า VIP ผู้มีอุปการคุณ" : "เรียน ลูกค้า";
}
}
// หลังแก้ (Good): ความรู้ "ใครคือ VIP" มีแหล่งเดียว — เป็น method บน Customer เอง
public class Customer
{
public decimal TotalPurchases { get; set; }
// ความรู้เรื่อง threshold ของ VIP อยู่ที่เดียวในระบบ
public bool IsVip => TotalPurchases > 100_000m;
}
public class OrderService
{
public decimal CalculateDiscount(Customer customer, decimal orderTotal) =>
customer.IsVip ? orderTotal * 0.10m : orderTotal * 0.02m;
}
public class ShippingService
{
public decimal CalculateShippingFee(Customer customer, decimal baseFee) =>
customer.IsVip ? 0m : baseFee;
}
public class NewsletterService
{
public string GetGreeting(Customer customer) =>
customer.IsVip ? "เรียน ลูกค้า VIP ผู้มีอุปการคุณ" : "เรียน ลูกค้า";
}

เมื่อ​วัน​หนึ่ง​ธุรกิจ​เปลี่ยน​เกณฑ์ VIP จาก 100,000 เป็น 150,000 บาท version “หลัง​แก้” แก้​ที่​เดียว​คือ Customer.IsVip แล้ว​ทุก service ที่​พึ่งพา​มัน​จะ​ถูกต้อง​พร้อม​กัน​ทันที ใน​ขณะ​ที่ version “ก่อน​แก้” ต้อง​ไล่​หา​ทุก​จุด​ที่​คัด​ลอก​ตรรกะ​ไว้ และ​เสี่ยง​พลาด​จุด​ใด​จุด​หนึ่ง

แผนภาพ​ด้าน​ล่าง​แสดง​ทิศทางการ​พึ่งพา​ความ​รู้​เดียวกัน แทนที่​แต่ละ service จะ​มี​สำเนา​กฎ​ของ​ตัวเอง ทุก service กลับ​พึ่งพา authoritative source เดียว

graph TD
    Order[OrderService] --> Rule[Customer dot IsVip]
    Shipping[ShippingService] --> Rule
    Newsletter[NewsletterService] --> Rule
  • block code ที่​แทบ​เหมือน​กัน​ทุก​ตัว​อักษร กระจาย​อยู่​ใน​หลาย file หรือ​หลาย class — สัญญาณ​คลาสสิก​ของ Duplicate Code
  • การ​แก้ bug ที่​ต้อง​ไล่​แก้​หลาย​จุด เพราะ code เดียวกัน​ถูกก็อปปี้​ไว้ แล้ว​ลืม​แก้​ครบ​ทุก​จุด
  • ค่า magic string/magic number ซ้ำ ๆ ที่​แทน​ความหมาย​เดียวกัน แต่​พิมพ์​แยก​กัน​คนละ​ที่ — ดู Magic Strings
  • นิสัย Copy-Paste Programming — เห็น code คล้าย​ที่​ต้องการ ก็อปมา​แล้ว​แก้​นิดหน่อย​แทนที่​จะ​ดึง​ออก​มา​เป็น abstraction ที่​ใช้​ร่วม​กัน ดู Copy-Paste Programming
  • ปน​กัน​ระหว่าง​หลาย abstraction level ใน method เดียว ซึ่ง​มัก​ทำให้​มอง​ไม่​เห็น​ความ​ซ้ำซ้อน​ที่​ซ่อน​อยู่ ดู Abstraction Levels smell
  • กระบวนการ deploy/test/build ที่​ยัง​ต้อง​ทำ​ด้วย​มือ​ซ้ำ ๆ ทุกรอบ ทั้ง​ที่ automate ได้ — DRY มิติ​กระบวนการ​ที่​มัก​ถูก​มอง​ข้าม
  • Once and Only Once — ถือ​เป็น​เซต​ย่อย​ของ DRY เน้น​กำจัด code ซ้ำ​โดยตรง
  • Open-Closed Principle — จะ​ทำงาน​ได้​ก็​ต่อ​เมื่อ​ยึด DRY เป็น​ฐาน
  • Single Responsibility Principle — ก็​อาศัย DRY เป็น​ฐาน​เช่น​กัน
  • Separation of Concerns — แยก​ความ​รับผิดชอบ​ให้​ชัด ช่วย​ให้​ความ​รู้​แต่ละ​ก้อน​มี​บ้าน​เดียว
  • Duplicate Code — code smell ที่​เป็น​อาการ​ของ​การ​ละเมิด DRY
  • Copy-Paste Programming — antipattern ต้นเหตุ​ของ​ความ​ซ้ำซ้อน​ที่​พบ​บ่อย​ที่สุด