Once and Only Once
พฤติกรรมแต่ละอย่าง นิยามเพียงครั้งเดียวและครั้งเดียวเท่านั้น
Once and Only Once (มักย่อว่า OAOO) ถือได้ว่าเป็นเซตย่อยของหลักการ Don’t Repeat Yourself และเป็นหนึ่งในหลักการพื้นฐานที่สุดของการพัฒนาซอฟต์แวร์ หลักการนี้มีรากมาจากวงการ Extreme Programming (XP) ในช่วงปลายทศวรรษ 1990 และเป็นหนึ่งในสี่ “Rules of Simple Design” ที่ Kent Beck วางไว้
แนวคิดหลัก
หัวข้อที่มีชื่อว่า “แนวคิดหลัก”Once and Only Once กล่าวโดยพื้นฐานว่า พฤติกรรมใด ๆ ภายใน application ควรถูกนิยาม เพียงครั้งเดียวและครั้งเดียวเท่านั้น (Once and Only Once) กล่าวอีกนัยหนึ่งคือ แนวคิดหรือ concept ที่เกี่ยวข้องแต่ละอย่างควรมี “ที่อยู่” ของมันใน code (ต้องถูกแสดงออกอย่างน้อยหนึ่งครั้ง — เพื่อสื่อความหมายให้ครบ) แต่ต้องไม่ถูกแสดงซ้ำในที่อื่นอีก (ต้องไม่เกินหนึ่งครั้ง) การซ้ำซ้อนของพฤติกรรมเป็นหนึ่งในแหล่ง bug ที่พบบ่อยที่สุดในระบบซอฟต์แวร์ เพราะเมื่อพฤติกรรมถูกนิยามไว้หลายที่ ยิ่งมีโอกาสมากขึ้นที่การเปลี่ยนพฤติกรรมในที่หนึ่งจะไม่ถูกส่งต่อไปยังทุกที่ที่พฤติกรรมนั้นถูกนิยามไว้
Kent Beck จัดลำดับความสำคัญของ Four Rules of Simple Design ไว้ดังนี้:
- Passes the tests — code ต้องทำงานถูกต้องตามที่ทดสอบไว้ก่อนเป็นอันดับแรก
- Reveals intention — code ต้องสื่อเจตนาของผู้เขียนอย่างชัดเจน
- No duplication — พูดทุกอย่าง Once and Only Once (นี่คือกฎข้อนี้)
- Fewest elements — ไม่มีส่วนเกินความจำเป็น
Martin Fowler อธิบายว่ากฎ “no duplication” นี้ “อาจเป็นกฎที่ทรงพลังอย่างแนบเนียนที่สุด” ในสี่ข้อ เพราะการลงมือกำจัดความซ้ำซ้อนอย่างจริงจังมักผลักดันให้ทีมค้นพบ abstraction ที่ดีขึ้นโดยไม่ได้ตั้งใจไว้ล่วงหน้า — พูดง่าย ๆ คือ “การไล่ล่าความซ้ำซ้อน” เป็นเทคนิคขับเคลื่อนการออกแบบที่ดี ไม่ใช่แค่กฎด้านความสะอาดของ code
ข้อควรระวังคือ OAOO ไม่เหมือนกับ DRY เสียทีเดียว: DRY เป็นหลักการกว้างกว่า ครอบคลุมทั้งความซ้ำซ้อนของ “ความรู้” (knowledge) ในทุกรูปแบบ ทั้งใน code เอกสาร process การตั้งค่า และฐานข้อมูล ส่วน OAOO เจาะจงไปที่ความซ้ำซ้อนของ พฤติกรรม/code โดยตรง จึงมักถูกมองว่าเป็นเซตย่อยของ DRY ที่ focus เฉพาะมิติของ code
การกำจัดความซ้ำซ้อนที่เกิดจากการไม่ยึดหลัก Once and Only Once เป็นหนึ่งในเหตุผลหลักของการ refactoring และยังเป็นแก่นของ design pattern จำนวนมากอีกด้วย — ไม่ว่าจะเป็น Template Method, Strategy หรือแม้แต่การใช้ inheritance/composition เพื่อดึงพฤติกรรมร่วมออกมาไว้ที่เดียว
ทำไมถึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมถึงสำคัญ”- ลดความเสี่ยงจาก bug shotgun surgery — เมื่อพฤติกรรมหนึ่งกระจายอยู่หลายที่ การแก้ bug หรือเปลี่ยน requirement หนึ่งอย่างจะต้องไล่แก้ทุกจุดที่ก๊อป code ไว้ หากลืมแม้แต่จุดเดียว ระบบจะมีพฤติกรรมไม่สอดคล้องกัน (inconsistent behavior) ทันที
- เพิ่มความเข้าใจได้ (understandability) — เมื่อพฤติกรรมหนึ่งอยู่ที่เดียว ผู้อ่าน code รู้ทันทีว่าต้องไปดูที่ไหนเพื่อเข้าใจว่าระบบทำงานอย่างไร ไม่ต้องเดาว่า “code ชุดนี้กับชุดนั้นยังตรงกันอยู่ไหม”
- ลด cost ของการเปลี่ยนแปลง — การเปลี่ยนพฤติกรรมกลายเป็นการแก้ไขจุดเดียว แทนที่จะต้องค้นหาทุกสำเนาทั่วทั้งระบบ ซึ่งลดทั้งเวลาและความเสี่ยงของ regression
- เป็นแรงขับเคลื่อนของการ refactoring ที่ดี — Fowler ชี้ว่าการพยายามกำจัดความซ้ำซ้อนอย่างจริงจังมักนำไปสู่การค้นพบ abstraction ใหม่ที่ทีมไม่เคยเห็นมาก่อน กล่าวคือ OAOO ไม่ใช่แค่ “งานทำความสะอาด” แต่เป็น “เครื่องมือออกแบบ”
- สอดคล้องกับ Single Responsibility — เมื่อพฤติกรรมหนึ่งอยู่ในที่เดียว มักหมายความว่ามันมี “เจ้าของ” (owner) ที่ชัดเจนหนึ่งเดียว ซึ่งใกล้เคียงกับเจตนารมณ์ของ Single Responsibility Principle
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ตัวอย่างต่อไปนี้แสดงกฎการคำนวณส่วนลดของคำสั่งซื้อ (order) ที่ถูก “ก๊อป” ไว้สามที่ในระบบ — ในตัวตรวจสอบราคาก่อนชำระเงิน, ใน invoice generator, และในหน้าสรุปตะกร้าสินค้า
// ก่อน: กฎ "ลูกค้า VIP ได้ส่วนลด 10% เมื่อยอดซื้อเกิน 1000 บาท" ถูกเขียนซ้ำสามที่
public class CheckoutService{ public decimal CalculateFinalPrice(Order order, Customer customer) { decimal total = order.Items.Sum(i => i.Price * i.Quantity); if (customer.IsVip && total > 1000m) { total *= 0.9m; // ส่วนลด 10% } return total; }}
public class InvoiceGenerator{ public decimal GetInvoiceTotal(Order order, Customer customer) { decimal total = order.Items.Sum(i => i.Price * i.Quantity); if (customer.IsVip && total > 1000m) // นิยามซ้ำ! ถ้ากฎเปลี่ยน ต้องมาแก้ตรงนี้ด้วย { total *= 0.9m; } return total; }}
public class CartSummaryViewModel{ public decimal GetDisplayedTotal(Order order, Customer customer) { decimal total = order.Items.Sum(i => i.Price * i.Quantity); if (customer.IsVip && total > 1000m) // นิยามซ้ำอีกครั้ง! { total *= 0.9m; } return total; }}หาก marketing เปลี่ยนกฎเป็น “ส่วนลด 15% เมื่อยอดเกิน 1500 บาท” นักพัฒนามีโอกาสสูงที่จะแก้ไม่ครบทั้งสามที่ ทำให้ราคาที่แสดงบนหน้าตะกร้า ใบแจ้งหนี้ และยอดที่ตัดจริง ไม่ตรงกัน — นี่คือความเสี่ยงที่ OAOO เตือนเรา
// หลัง: ดึงกฎส่วนลดออกมานิยามไว้ Once and Only Once
public class VipDiscountPolicy{ private const decimal DiscountThreshold = 1000m; private const decimal DiscountRate = 0.10m;
public decimal Apply(decimal subtotal, Customer customer) { bool qualifies = customer.IsVip && subtotal > DiscountThreshold; return qualifies ? subtotal * (1 - DiscountRate) : subtotal; }}
public class OrderPricer{ private readonly VipDiscountPolicy _discountPolicy;
public OrderPricer(VipDiscountPolicy discountPolicy) { _discountPolicy = discountPolicy; }
public decimal CalculateTotal(Order order, Customer customer) { decimal subtotal = order.Items.Sum(i => i.Price * i.Quantity); return _discountPolicy.Apply(subtotal, customer); }}
// CheckoutService, InvoiceGenerator และ CartSummaryViewModel// ทุกที่เรียกใช้ OrderPricer ตัวเดียวกัน — กฎมีที่อยู่เดียว แก้ที่เดียว ใช้ได้ทุกที่โครงสร้างหลังการ refactor ทำให้เห็นทิศทางของการพึ่งพา: ผู้ใช้กฎ (consumer) หลายจุดต่างเรียก policy กลางที่เดียว แทนที่จะนิยามกฎซ้ำในตัวเอง
graph TD
Checkout[CheckoutService] --> Pricer[OrderPricer]
Invoice[InvoiceGenerator] --> Pricer
Cart[CartSummaryViewModel] --> Pricer
Pricer --> Policy[VipDiscountPolicy]
สัญญาณว่ากำลังละเมิดหลักการนี้
หัวข้อที่มีชื่อว่า “สัญญาณว่ากำลังละเมิดหลักการนี้”- Copy-paste เป็นวิธีเริ่มต้น feature ใหม่ — เห็น code คล้ายเดิมเกือบทุกตัวอักษร แค่เปลี่ยนตัวแปรไม่กี่ตัว
- bug ที่ “แก้แล้วแต่ยังไม่หาย” — เพราะแก้ผิดสำเนา หรือแก้ได้แค่บางสำเนาจากหลาย ๆ ที่ที่พฤติกรรมเดียวกันถูกนิยามไว้
- ต้องแก้หลาย file เพื่อเปลี่ยน requirement เดียว — อาการคลาสสิกของ shotgun surgery
- Magic number/constant เดียวกันปรากฏกระจัดกระจาย เช่น อัตราภาษี, threshold, หรือ regex validation ที่ hardcode ซ้ำในหลาย class
- Business rule เดียวกันถูกเขียนคนละภาษา/คนละ layer เช่น validation rule ซ้ำทั้งฝั่ง client และฝั่ง server โดยไม่มี single source of truth ร่วมกัน (แม้บางกรณีจำเป็นต้องมี2 implementation ด้วยเหตุผลทางเทคนิค แต่ก็ควรรู้ตัวว่ากำลังยอมรับความเสี่ยงนี้)
- สับสนกับกรณีข้อยกเว้นที่ยอมรับได้ — เช่น การที่ fact เดียวกันปรากฏทั้งใน unit test, functional test และในโปรแกรมจริง ไม่ถือเป็นการละเมิด OAOO เพราะ test คือการยืนยันพฤติกรรมจากมุมมองภายนอก ไม่ใช่การนิยามพฤติกรรมซ้ำ
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Don’t Repeat Yourself
- Single Responsibility Principle
- Duplicate Code
- Shotgun Surgery
- Extract Method — Refactor
- Copy and Paste Programming