Event Aggregator
ฮับกลางที่รวบรวม event จากหลายแหล่งแล้วส่งต่อผู้สนใจ
ลองนึกภาพหน้าจอ dashboard ที่มี widget สิบกว่าตัว แต่ละตัวต้องรู้เมื่อผู้ใช้เปลี่ยนตัวกรอง เมื่อออเดอร์ใหม่เข้ามา หรือเมื่อผู้ใช้ล็อกเอาต์ ถ้าใช้ Observer ตรง ๆ แต่ละ widget ต้องไปลงทะเบียน (subscribe) กับ source object ทุกตัวที่ตัวเองสนใจ เมื่อจำนวน source และ observer เพิ่มขึ้น จำนวนความสัมพันธ์แบบ many-to-many ก็ระเบิดตาม — code ที่ผูก event handler กระจายอยู่ทั่วระบบ และการจัดการ memory (ลืม unsubscribe แล้ว leak) ก็ยุ่งยากขึ้นเรื่อย ๆ
Event Aggregator แก้ปัญหานี้ด้วยการใส่ตัวกลางหนึ่งตัวคั่นระหว่าง publisher กับ subscriber ทั้งหมด แนวคิดหลักคือ “รวม event จากหลายแหล่งเข้าเป็นแหล่งเดียว” — Martin Fowler อธิบายไว้ว่า Event Aggregator ทำหน้าที่เป็น “single source of events for many objects” มันลงทะเบียนรับ event จาก source ทุกตัวแทน client แล้วส่งต่อ (หรือแปลงให้เป็น event ที่ generic ขึ้น) ไปยัง subscriber ที่สนใจ ผลคือ publisher ไม่รู้จัก subscriber และ subscriber ไม่รู้จัก publisher เลย ทั้งสองฝ่ายรู้จักแค่ aggregator ตรงกลาง ทำให้ระบบ event-driven ที่ decouple สูง ขยายง่าย และลด boilerplate การลงทะเบียนซ้ำ ๆ
โดยทั่วไป aggregator ถูก implement เป็น singleton หรือ scoped service ที่ inject ไปยังทุกจุดที่ต้อง publish หรือ subscribe เช่นในระบบ eCommerce การสั่งซื้อหนึ่งครั้งอาจจุดชนวนหลายปฏิกิริยาอิสระที่ไม่เกี่ยวข้องกัน (ส่งอีเมล อัปเดตสต็อก บันทึก audit log) โดยที่ handler สั่งซื้อไม่ต้องรู้จักและไม่ต้องเรียกใช้ทั้งสามระบบนั้นโดยตรง
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class IEventAggregator {
<<interface>>
+Publish(evt)
+Subscribe(handler)
+Unsubscribe(handler)
}
class EventAggregator {
-handlers Dictionary
+Publish(evt)
+Subscribe(handler)
+Unsubscribe(handler)
}
class Publisher {
-aggregator IEventAggregator
+DoWork()
}
class SubscriberA {
+Handle(evt)
}
class SubscriberB {
+Handle(evt)
}
class DomainEvent {
<<marker>>
}
IEventAggregator <|.. EventAggregator
Publisher o-- IEventAggregator
SubscriberA o-- IEventAggregator
SubscriberB o-- IEventAggregator
EventAggregator ..> DomainEvent
Publisher และ Subscriber ต่างก็ถือ reference ไปยัง IEventAggregator เท่านั้น ไม่มีฝ่ายไหนอ้างอิงถึงกันโดยตรง — นี่คือจุดต่างหลักจาก Observer แบบดั้งเดิมที่ subject ต้องถือ list ของ observer โดยตรง
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”ลำดับการทำงานทั่วไปมีสามขั้น: (1) subscriber ลงทะเบียน handler กับ aggregator ล่วงหน้า (2) publisher เรียก Publish เมื่อมีเหตุการณ์เกิดขึ้น (3) aggregator วนหา handler ที่ตรง type แล้วเรียกทีละตัว โดยปกติ aggregator “ไม่มี logic ทางธุรกิจ” เลย — มันแค่ forward event จาก publisher ไปหา subscriber แบบ fire-and-forget เท่านั้น ถ้าตัวกลางเริ่มมีการตัดสินใจหรือ orchestrate ลำดับขั้นตอนทางธุรกิจ นั่นคือสัญญาณว่ากำลังกลายเป็น Mediator ไปแล้ว ไม่ใช่ Event Aggregator อีกต่อไป
sequenceDiagram
participant P as Publisher
participant A as EventAggregator
participant S1 as SubscriberA
participant S2 as SubscriberB
S1->>A: Subscribe(Handle)
S2->>A: Subscribe(Handle)
P->>A: Publish(OrderPlaced)
A->>S1: Handle(OrderPlaced)
A->>S2: Handle(OrderPlaced)
หมายเหตุเรื่อง memory management: เพราะ subscriber ลงทะเบียนกับ aggregator เพียงจุดเดียว การ unsubscribe (เช่นตอน dispose ของ ViewModel) ก็ทำที่จุดเดียวเช่นกัน ต่างจาก Observer ตรง ๆ ที่ต้อง unsubscribe จาก source object ทุกตัวแยกกัน ซึ่งลืมง่ายและเป็นสาเหตุ memory leak บ่อยครั้ง
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”// สัญญาที่ publisher และ subscriber ต่างพึ่งพา — ไม่มีใครรู้จักกันโดยตรงpublic interface IEventAggregator{ void Publish<TEvent>(TEvent evt); void Subscribe<TEvent>(Action<TEvent> handler); void Unsubscribe<TEvent>(Action<TEvent> handler);}
// การ implement แบบง่าย เก็บ handler แยกตาม type ของ eventpublic sealed class EventAggregator : IEventAggregator{ private readonly Dictionary<Type, List<Delegate>> _handlers = new();
public void Subscribe<TEvent>(Action<TEvent> handler) { var eventType = typeof(TEvent); if (!_handlers.TryGetValue(eventType, out var list)) { list = new List<Delegate>(); _handlers[eventType] = list; } list.Add(handler); }
public void Unsubscribe<TEvent>(Action<TEvent> handler) { if (_handlers.TryGetValue(typeof(TEvent), out var list)) { list.Remove(handler); } }
public void Publish<TEvent>(TEvent evt) { if (!_handlers.TryGetValue(typeof(TEvent), out var list)) { return; }
// ทำสำเนา list ก่อนวน loop กันปัญหา handler unsubscribe ตัวเองระหว่าง publish foreach (var handler in list.ToArray()) { ((Action<TEvent>)handler).Invoke(evt); } }}
// event ที่ใช้เป็น payload — เป็น record แบบ immutablepublic sealed record OrderPlaced(Guid OrderId, decimal Total);
// Publisher: OrderService รู้จักแค่ IEventAggregator ไม่รู้จัก subscriber เลยpublic sealed class OrderService{ private readonly IEventAggregator _events;
public OrderService(IEventAggregator events) => _events = events;
public void PlaceOrder(Guid orderId, decimal total) { // ... บันทึกออเดอร์ลงฐานข้อมูลจริง ... _events.Publish(new OrderPlaced(orderId, total)); }}
// Subscriber ตัวอย่าง: แต่ละตัวทำงานอิสระ ไม่ผูกกับ OrderService หรือกันเองpublic sealed class InventoryUpdater{ public InventoryUpdater(IEventAggregator events) => events.Subscribe<OrderPlaced>(OnOrderPlaced);
private void OnOrderPlaced(OrderPlaced evt) { // ตัดสต็อกตามออเดอร์ที่เพิ่งเข้ามา }}
public sealed class OrderEmailNotifier{ public OrderEmailNotifier(IEventAggregator events) => events.Subscribe<OrderPlaced>(evt => SendReceiptEmail(evt.OrderId));
private void SendReceiptEmail(Guid orderId) { /* ส่งอีเมลใบเสร็จ */ }}เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- ระบบมี publisher และ subscriber จำนวนมากที่ไม่ควรรู้จักกันโดยตรง (เช่น UI แบบ modular ที่แต่ละ module พัฒนาแยกทีมกัน)
- ต้องการลด boilerplate การลงทะเบียน event เมื่อ source object มีจำนวนมากและเปลี่ยนแปลงบ่อย
- ต้องการจุดเดียวสำหรับ unsubscribe ทั้งหมด เพื่อลดความเสี่ยง memory leak จาก event handler ที่ค้าง
- งาน UI (WPF, MAUI, SPA) ที่ ViewModel หลายตัวต้องสื่อสารกันแบบ loosely coupled โดยไม่ผ่าน parent-child hierarchy
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- มี publisher/subscriber เพียงคู่เดียวหรือสองสามคู่ — ใช้ event ปกติของภาษา (C#
eventkeyword) หรือ Observer ตรง ๆ ก็เพียงพอและอ่านง่ายกว่า - ต้องการ orchestrate ลำดับขั้นตอนทางธุรกิจที่ซับซ้อน (เช่น step A ต้องเสร็จก่อน step B ถึงจะเริ่ม step C) — นั่นคืองานของ Mediator ไม่ใช่ event aggregator ที่ควรไม่มี logic เลย
- ทีมยังไม่มีวินัยในการตั้งชื่อและจัดหมวด event — เพราะทุกอย่างถูกยิงผ่านฮับเดียว การ debug ว่า “ใคร publish เมื่อไหร่ ใคร subscribe อะไร” จะทำได้ยากถ้าไม่มี convention หรือ tooling ช่วย
- ต้องการ delivery guarantee ข้ามกระบวนการ/เครื่อง (durable, retry, ordering) — นั่นคืองานของ message broker หรือ Domain Events ที่ persist แล้ว dispatch แบบมี transaction ไม่ใช่ในกระบวนการเดียวแบบ in-memory
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | decouple publisher กับ subscriber อย่างสมบูรณ์ ไม่มีฝ่ายไหนอ้างอิงกัน |
| ข้อดี | ลดจำนวนจุดลงทะเบียนจาก many-to-many เหลือ many-to-one |
| ข้อดี | unsubscribe รวมศูนย์ที่จุดเดียว ลดความเสี่ยง memory leak |
| ข้อดี | เพิ่ม/ลบ subscriber ใหม่ได้โดยไม่กระทบ publisher เลย เหมาะกับระบบที่ขยายบ่อย |
| ข้อเสีย | เพิ่ม indirection — ตามรอย flow ของ event ยากขึ้น เพราะไม่เห็น “ใครเรียกใคร” ตรง ๆ ใน code |
| ข้อเสีย | ถ้าตั้งชื่อ event ไม่ดีหรือไม่มี convention จะกลายเป็น global event bus ที่ debug ยากมาก |
| ข้อเสีย | โดยธรรมชาติเป็น in-memory, synchronous ในกระบวนการเดียว — ไม่เหมาะกับงานข้ามระบบ/ข้าม process |
| ข้อเสีย | ตัว aggregator เองมักถูก inject ไปทั่วระบบ เสี่ยงกลายเป็น service locator แฝง ถ้าใช้พร่ำเพรื่อ |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Observer — Event Aggregator เป็นกรณีพิเศษที่รวม Observer เข้ากับ Mediator บางส่วน
- Mediator — คล้ายกันตรงเป็นตัวกลาง แต่ Mediator มี logic ทางธุรกิจ ส่วน Event Aggregator ไม่ควรมี
- Domain Events — รูปแบบ event ที่เจาะจงกับ domain model และมักถูก dispatch ผ่านกลไกคล้าย aggregator
- Singleton — aggregator มักถูก implement เป็น singleton หรือ scoped service เดียวต่อ app
- Dependency Inversion Principle — publisher และ subscriber พึ่งพา
IEventAggregatorซึ่งเป็น abstraction ไม่ใช่กันและกันโดยตรง - Service Locator — antipattern ที่ใกล้เคียงกันถ้า inject aggregator แบบไม่มีวินัยจนกลายเป็นจุดเข้าถึง service ทุกอย่าง
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/design-patterns/event-aggregator-pattern
- Event Aggregator — Martin Fowler
- Event Aggregator And/Or/vs Mediator: A Tale Of Two Patterns — Derick Bailey, Los Techies
- Event Aggregator: An implementation in C# — Aria Mohassel, Medium
- Software Design: Mediator vs Observer/Publish-Subscribe vs Event Aggregator