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

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 ไว้​ดังนี้:

  1. Passes the tests — code ต้อง​ทำงาน​ถูกต้อง​ตาม​ที่​ทดสอบ​ไว้​ก่อน​เป็น​อันดับ​แรก
  2. Reveals intention — code ต้อง​สื่อ​เจตนา​ของ​ผู้​เขียน​อย่าง​ชัดเจน
  3. No duplication — พูด​ทุก​อย่าง Once and Only Once (นี่​คือ​กฎ​ข้อ​นี้)
  4. 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 คือ​การ​ยืนยัน​พฤติกรรม​จาก​มุมมอง​ภายนอก ไม่ใช่​การ​นิยาม​พฤติกรรม​ซ้ำ