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