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 รับประกันความถูกต้องตั้งแต่ตอนสร้าง
หัวข้อที่มีชื่อว่า “Value Object รับประกันความถูกต้องตั้งแต่ตอนสร้าง”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 รับประกันความถูกต้องไว้ล่วงหน้า)
Facade เป็นประตูเดียวสำหรับ subsystem
หัวข้อที่มีชื่อว่า “Facade เป็นประตูเดียวสำหรับ subsystem”เมื่อการดำเนินการหนึ่งต้องประสานงานหลาย 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
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Don’t Repeat Yourself
- Encapsulation
- Make Illegal States Unrepresentable
- Aggregate
- Facade Pattern
- Defensive Programming