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

Single Point of Enforcement

ให้​ทุก​การกระทำ​ที่​ต้อง​เป็น​ไป​ตาม​กฎ​วิ่ง​ผ่าน​ประตู​เดียว

Single Point of Enforcement (SPE) คือ​แนวทาง​ออกแบบ​ที่ รวม​ศูนย์​การ​บังคับ​ใช้​กฎ (rule enforcement) ของ​การ​ดำเนิน​การ​ชนิด​หนึ่ง ๆ ไว้​ที่​จุด​เดียว​ใน​ระบบ แทนที่​จะ​กระจาย validation logic เดียวกัน​ไป​ตาม​หลาย ๆ ที่ (controller, service, UI, batch job) จุด​นั้น​ทำ​หน้าที่​เป็น “ผู้​เฝ้า​ประตู” (gatekeeper) ที่​รับประกัน​ว่า​ทุก​คำขอ​เปลี่ยน​สถานะ จะ​ถูก validate ตรวจ invariant และ​บังคับ​ใช้​กฎ​ธุรกิจ​อย่าง​ครบถ้วน​และ​สม่ำเสมอ ไม่​ว่า​คำขอ​นั้น​จะ​มา​จาก​ทาง​ไหน​ก็ตาม

แนวคิด​นี้​เป็น​ส่วน​ขยาย​เชิง​ปฏิบัติ​ของ​หลักการ Don’t Repeat Yourself เมื่อ DRY บอกว่า “ความ​รู้​แต่ละ​ชิ้น​ควร​มี​ตัวแทน​ที่​ชัดเจน​เพียง​หนึ่ง​เดียว​ใน​ระบบ” SPE คือ​การนำ​หลักการ​นี้​ไป​ใช้​กับ กฎ​และ​การ enforcement โดย​เฉพาะ — ถ้า​กฎ “ยอด​คำ​สั่ง​ซื้อ​ห้าม​ติดลบ” ถูก​เช็ค​ซ้ำ​ใน​สาม​ที่ วัน​หนึ่ง​เมื่อ​กฎ​เปลี่ยน​หรือ​มี​คน​แก้​ที่​เดียว​แล้ว​ลืม​อีก​สอง​ที่ ระบบ​ก็​จะ​เข้า​สู่​สถานะ​ที่​ไม่​สอดคล้อง​กัน​ทันที

แนวคิด​ที่​ใกล้​เคียง​กัน​ใน​โลก​ความ​ปลอดภัย​คือ Policy Enforcement Point (PEP) ใน​สถาปัตยกรรม access control — PEP ดัก​จับ​ทุก​คำขอ​เข้าถึง​ทรัพยากร ส่ง​ไป​ให้ Policy Decision Point ตัดสิน แล้ว​จึง​อนุญาต​หรือ​ปฏิเสธ หลักการ​เดียวกัน​นี้​ถูก​นำ​มา​ใช้​ใน​ระดับ domain ของ application: แทนที่​จะ​ให้​ทุก​จุด​ใน code ตัดสิน​ใจ​เอง​ว่า​กฎ​คือ​อะไร ให้​มี “ประตู” เดียว​ที่​รู้​กฎ​ทั้งหมด​และ​บังคับ​ใช้​อย่าง​เข้มงวด

ใน Domain-Driven Design, Aggregate คือ​รูปแบบ​ที่​ชัดเจน​ที่สุด​ของ SPE — aggregate root เป็น​จุด​เข้า​เดียว​สำหรับ​เปลี่ยนแปลง entity ภายใน อ้างอิง​จาก​ภายนอก​ไป​ยัง object ใน​กลุ่ม​ควร​ผ่าน root เท่านั้น ทำให้ root เป็น​ผู้​พิทักษ์ (guardian) ของ invariant ทั้งหมด​ใน​ขอบเขต​นั้น หาก caller ข้าม root แล้ว​แก้ entity ภายใน​โดยตรง root ก็​ไม่มี​ทาง​รับประกัน​ความ​ถูกต้อง​ของ​ข้อมูล​ได้​อีก​ต่อ​ไป

การนำ SPE ไป​ปฏิบัติ​จริง​มี​องค์​ประกอบ​หลักสี่​อย่าง

  • Centralized Control — กำหนด​จุด​เข้า (entry point) เดียว​สำหรับ​การ​ดำเนิน​การ​ชนิด​หนึ่ง ๆ เช่น method บน aggregate root, command handler เดียว, หรือ service ชั้น​เดียว
  • Validation & Enforcement — gatekeeper นั้น​เป็น​เจ้าของ​กฎ​ทั้งหมด ทั้ง input validation และ business invariant แล้ว apply กับ​ทุก​คำขอ​เหมือน​กัน​หมด ไม่มี​เงื่อนไข​พิเศษ​ที่​ยกเว้น​บาง​เส้นทาง
  • Uniform Execution — ปิด​ทาง​ลัด​ทุก​ทาง เช่น ทำ field เป็น private set, ปิด public constructor ที่​ข้าม validation, ไม่ expose collection ภายใน​ให้​แก้ไข​ตรง ๆ (ดู Encapsulation)
  • Enforcement ของ​ตัว SPE เอง — ต่อ​ให้​ออกแบบ​ดี​แค่​ไหน นัก​พัฒนา​ใน​อนาคต​ก็​อาจ​เผลอ​เปิด​ทาง​ลัด​โดย​ไม่​ตั้งใจ จึง​ควร​มี automated check (architecture test, code review checklist, static analysis rule) คอย​เตือน​เมื่อ​มี​ใคร​พยายาม​แก้ state โดย​ไม่​ผ่าน​ประตู

รูปแบบ (pattern) ที่​มัก​ถูก​ใช้​เป็น​กลไก​ของ SPE ได้แก่ Aggregate และ Value Object ใน DDD, Command Handler, Facade Pattern, Repository ที่​ซ่อน​รายละเอียด​การ​เข้าถึง​ข้อมูล และ​แม้แต่ database constraint ที่​บังคับ invariant ใน​ระดับ storage เป็น​ด่าน​สุดท้าย

flowchart LR
  Caller[Caller เช่น API, Job, UI] --> Gate[Aggregate Root ประตูเดียว]
  Gate --> Check{Validate และ Invariant ผ่านไหม}
  Check -->|ผ่าน| Apply[เปลี่ยนสถานะภายใน]
  Check -->|ไม่ผ่าน| Reject[โยน Exception ปฏิเสธคำขอ]
  Apply --> Store[Persist ผ่าน Repository]

ข้อ​ควร​ระวัง​ใน​การ​ออกแบบ: SPE ไม่​ได้​แปล​ว่า​ทุก​อย่าง​ใน​ระบบ​ต้อง​ผ่าน​จุด​เดียว​จุด​เดียวกัน​หมด แต่​หมาย​ถึง กฎ​แต่ละ​ข้อ ควร​มี​จุด​บังคับ​ใช้​เพียง​จุด​เดียว ระบบ​ขนาด​ใหญ่​จึง​มัก​มี SPE หลาย​จุด — จุด​หนึ่ง​ต่อ1 aggregate หรือ​หนึ่ง​ชุด​ของ invariant ที่​เกี่ยวข้อง​กัน ไม่ใช่ god object เดียว​ที่​รู้​ทุก​กฎ​ใน​ระบบ (ซึ่ง​จะ​กลาย​เป็น Blob)

Aggregate root เป็น​ประตู​เดียว​สำหรับ​แก้ไข​รายการ​สินค้า

หัวข้อ​ที่​มีชื่อ​ว่า “Aggregate root เป็น​ประตู​เดียว​สำหรับ​แก้ไข​รายการ​สินค้า”

ใน DDD, aggregate root เป็น​จุด​เข้า​เดียว​ที่​การ​ดำเนิน​การ​ทั้งหมด​ต้อง​ผ่าน เพื่อ​รับประกัน​ว่าการ​เปลี่ยนแปลง​ต่อ entity ภายใน​ถูก validate และ​สอดคล้อง​กับ​กฎ​ธุรกิจ

public class Order : AggregateRoot
{
private readonly List<OrderItem> _orderItems = new();
public IReadOnlyCollection<OrderItem> Items => _orderItems.AsReadOnly();
public void AddItem(OrderItem item)
{
// กฎธุรกิจ: ห้ามเพิ่มสินค้าซ้ำ
if (_orderItems.Any(i => i.ProductId == item.ProductId))
throw new InvalidOperationException("Item already added.");
_orderItems.Add(item);
}
// การแก้ไข Order ทั้งหมดต้องผ่าน aggregate root เท่านั้น
// ไม่มี method ใดที่คืนค่า List แบบ mutable ออกไปให้ caller แก้เอง
}

เพราะ _orderItems เป็น private และ​คืน​ค่า​ออก​ไป​เป็น IReadOnlyCollection การ​เพิ่มสินค้า​จึง​มี​ทาง​เดียว​คือ​ผ่าน AddItem — ไม่มี​ทาง​ที่ caller จะ​เพิ่มสินค้า​ซ้ำ​โดย​ไม่​ผ่าน​การ​ตรวจ (เทียบ​กับ antipattern Exposing Collections)

Value Object ห่อ​หุ้ม​แนวคิด​และ​พฤติกรรม​เฉพาะ รวม​ศูนย์​การ​ดำเนิน​การ​อย่าง validation, formatting และ​การ​เปรียบเทียบ พร้อม​บังคับ immutability

public class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
// ทุกการดำเนินการที่เกี่ยวกับเงินต้องเคารพกฎเหล่านี้
if (amount < 0)
throw new ArgumentException("Amount must be non-negative.");
if (string.IsNullOrEmpty(currency))
throw new ArgumentException("Currency must be specified.");
Amount = amount;
Currency = currency;
}
}

เพราะ Money ไม่มี setter และ constructor เป็น​ทาง​เดียว​ใน​การ​สร้าง instance กฎ “จำนวน​เงิน​ต้อง​ไม่​ติดลบ” จึง​ถูก​บังคับ​ใช้​ที่​จุด​เดียว​เสมอ ไม่​ว่า Money จะ​ถูก​สร้าง​จาก​ที่ไหน​ใน​ระบบ (แนวคิด​นี้​สัมพันธ์​โดยตรง​กับ Make Illegal States Unrepresentable — แทนที่​จะ​เช็ค​ซ้ำ​ทุก​ครั้ง​ก่อน​ใช้ ให้ type รับประกัน​ความ​ถูกต้อง​ไว้​ล่วงหน้า)

เมื่อ​การ​ดำเนิน​การ​หนึ่ง​ต้อง​ประสาน​งาน​หลาย service (เช่น เช็ค inventory, เรียก payment gateway, ส่ง event) Facade Pattern ทำ​หน้าที่​เป็น SPE ระดับ subsystem ให้ caller เห็น method เดียว โดย​ลำดับ​ขั้นตอน​และ​กฎ​การ​ประสาน​งาน​ถูก​ซ่อน​และ​บังคับ​ใช้​อยู่​ภายใน​จุด​เดียว ไม่​กระจาย​ไป​ให้ caller แต่ละ​ราย​ต้อง​จำ​เอง​ว่า​ต้อง​เรียก​อะไร​ก่อน​หลัง

ประโยชน์

  • ความ​สม่ำเสมอ (consistency) — ทุก​คำขอ​ถูก​ตรวจ​ด้วย​กฎ​ชุด​เดียวกัน ไม่มี​เส้นทาง​ไหน​หลุด​รอด
  • แก้ไข​ที่​เดียว​จบ — เมื่อ​กฎ​ธุรกิจ​เปลี่ยน แก้​ที่​จุด​เดียว ไม่​ต้อง​ไล่​หา​ทุก​ที่​ที่ copy-paste ตรรกะ​ไว้ (หลีก​เลี่ยง Duplicate Code)
  • ลด​พื้นที่​การ​ทดสอบ — test กฎ​ธุรกิจ​ที่ gatekeeper จุด​เดียว ก็​มั่นใจ​ได้​ว่า​ทุก​เส้นทาง​ที่​ผ่าน​มัน​ถูก​คุ้มครอง
  • debug ง่าย​ขึ้น — เมื่อ​พบ​ข้อมูล​ผิด​กฎ รู้ทันที​ว่า​ต้อง​สงสัย​จุด​ไหน​ก่อน เพราะ​มี​จุด​เดียว​ที่ “ควร” เป็น​ผู้รับผิดชอบ

ข้อ​ควร​ระวัง

  • ทาง​ลัด​ที่​มอง​ไม่​เห็น — ORM ที่​ทำ reflection เข้าไป​ตั้ง​ค่า private field ตรง ๆ, migration script ที่​เขียน​ข้อมูล​ลง DB โดย​ไม่​ผ่าน domain, หรือ admin tool ฉุกเฉิน​ที่ bypass validation ล้วน​ทำลาย SPE ได้​ทั้งสิ้น​แม้ code หลัก​จะ​ออกแบบ​ถูกต้อง
  • คอ​ขวด​ของ performance หรือ​การ​พัฒนา — ถ้า SPE ถูก​ออกแบบ​กว้าง​เกิน​ไป​จน​กลาย​เป็น​จุด​เดียว​ที่​ทุก​ทีม​ต้อง​แก้ไข อาจ​กลาย​เป็น​คอ​ขวด​ทั้ง​ด้าน throughput และ merge conflict ทางออก​คือ​แบ่ง aggregate ให้​เล็ก​และ​มี​ขอบเขต​ชัดเจน​ตาม invariant จริง ไม่ใช่​รวม​ทุก​อย่าง​ไว้​ที่​เดียว
  • สับสน​กับ Singleton — SPE พูด​ถึง “จุด​บังคับ​ใช้​กฎ​เดียว” ไม่ใช่ “instance เดียว​ใน​หน่วย​ความ​จำ” อย่า​เข้าใจ​ผิด​ว่า​ต้อง​ใช้ Singleton เพื่อ​ทำ SPE เสมอ​ไป
  • การกระจาย SPE ข้าม​ระบบ​แบบ distributed — เมื่อ​ระบบ​แตก​เป็น microservices ต้อง​คิด​ให้​ชัด​ว่า​กฎ​แต่ละ​ข้อ “เป็น​เจ้าของ” (owned) โดย service ไหน แล้ว​ให้ service อื่น​เรียก​ผ่าน API แทนที่​จะ implement กฎ​ซ้ำ​ใน​แต่ละ service