Mediator
ให้ component สื่อสารผ่านตัวกลาง แทนการรู้จักกันโดยตรง
เมื่อ object หลายตัวในระบบต้องคุยกัน ทางเลือกแรกที่มักเกิดขึ้นเองตามธรรมชาติคือให้แต่ละตัว อ้างอิงถึงกันโดยตรง — checkbox รู้จัก textbox, textbox รู้จักปุ่ม submit, ปุ่ม submit รู้จัก validator ฯลฯ ปัญหาคือความสัมพันธ์แบบนี้เติบโตเป็น n×n อย่างรวดเร็ว: เพิ่ม component ใหม่หนึ่งตัวอาจต้องแก้ code ใน component เดิมหลายตัว และแทบเป็นไปไม่ได้ที่จะนำ component ตัวใดตัวหนึ่งไปใช้ซ้ำในบริบทอื่น เพราะมันลาก dependency ของเพื่อนบ้านติดไปด้วย
Mediator (จาก Gang of Four) แก้ปัญหานี้ด้วยการดึงตรรกะการประสานงานทั้งหมดออกมาไว้ใน object ตัวกลางตัวเดียว — ตัว mediator เอง component แต่ละตัวไม่ต้องรู้จักกันอีกต่อไป มันแค่ แจ้ง mediator ว่าเกิดอะไรขึ้นกับตัวมันเอง (เช่น “ค่าเปลี่ยนแล้ว”, “ถูกกดแล้ว”) แล้วปล่อยให้ mediator ตัดสินใจว่าจะ “กระจาย” ผลกระทบนั้นไปยัง component ตัวไหนต่อ ผลคือความสัมพันธ์แบบ n×n ถูกยุบเหลือ n (แต่ละ component คุยกับ mediator แค่เส้นเดียว) แนวคิดนี้สอดคล้องโดยตรงกับ Law of Demeter — “อย่าคุยกับคนแปลกหน้า” เพราะ component ไม่จำเป็นต้องรู้โครงสร้างภายในของเพื่อนบ้านอีกเลย
Mediator ใช้บ่อยมากใน code UI (dialog box ที่ widget หลายตัวต้อง enable/disable ตามกัน) และในระบบสื่อสารระหว่าง module เช่น chat room ที่ผู้ใช้แต่ละคนไม่คุยกันตรง ๆ แต่ส่งผ่านห้อง (mediator) เสมอ
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Mediator {
+Notify(sender, ev)
}
class ConcreteMediator {
-ComponentA componentA
-ComponentB componentB
+Notify(sender, ev)
}
class BaseComponent {
#Mediator mediator
+SetMediator(m)
}
class ComponentA {
+DoA()
}
class ComponentB {
+DoB()
}
Mediator <|.. ConcreteMediator
BaseComponent o-- Mediator
ComponentA --|> BaseComponent
ComponentB --|> BaseComponent
ConcreteMediator --> ComponentA
ConcreteMediator --> ComponentB
ผู้เกี่ยวข้องในโครงสร้าง:
- Mediator — interface ที่ประกาศจุดรับแจ้งเหตุการณ์ (มักเป็นเมท็อดเดียวอย่าง
Notify) - ConcreteMediator — class ที่ implement
Mediatorเก็บ reference ไปยัง component ที่เกี่ยวข้องทั้งหมด และมี “ตรรกะทางธุรกิจ” ว่าเหตุการณ์แบบไหนควรทำให้ component ไหนตอบสนองอย่างไร - BaseComponent / Component — class ที่เก็บ reference กลับไปยัง mediator เพียงเส้นเดียว ไม่รู้จัก component อื่นเลย
- ลูกศร
ConcreteMediator --> ComponentA/Bแสดงว่า mediator เป็นฝ่ายรู้จักและควบคุม component ทั้งหมด ในขณะที่ component รู้จักแค่ mediator ผ่านความสัมพันธ์แบบ aggregation (o--)
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”ลำดับการทำงานทั่วไปคือ: component หนึ่งเกิดเหตุการณ์บางอย่าง (เช่น ผู้ใช้กดปุ่ม หรือค่า field เปลี่ยน) → component นั้นเรียก mediator.Notify(this, "eventName") โดยไม่รู้ว่าใครจะตอบสนอง → mediator ตรวจสอบว่า sender และ event คืออะไร แล้วตัดสินใจเรียกเมท็อดของ component อื่นตามตรรกะที่กำหนดไว้ล่วงหน้า
sequenceDiagram
participant ComponentA
participant ConcreteMediator
participant ComponentB
ComponentA->>ConcreteMediator: Notify(this, "changed")
ConcreteMediator->>ConcreteMediator: ตัดสินใจว่าใครควรตอบสนอง
ConcreteMediator->>ComponentB: DoB()
ComponentB-->>ConcreteMediator: เสร็จสิ้น
จุดสำคัญคือ ComponentA ไม่เคยเรียก ComponentB โดยตรง — มันรู้แค่ว่ามี mediator อยู่ ส่วน mediator เป็นฝ่ายเดียวที่รู้ทั้ง “แผนที่” ของระบบว่าใครควรได้รับผลกระทบจากเหตุการณ์ไหนบ้าง ทำให้เพิ่ม/ลด/แก้ไข component ได้โดยแตะแค่ตัว mediator ไม่ต้องไล่แก้ทุก component ที่เกี่ยวข้อง
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่างขยายจากกระบวนการสั่งซื้อสินค้า: เมื่อชำระเงินสำเร็จ ต้องเริ่มจัดส่งและแจ้งเตือนลูกค้า — โดยที่ PaymentComponent ไม่รู้จัก ShippingComponent หรือ NotificationComponent เลยแม้แต่น้อย
// interface mediator: ประกาศจุดแจ้งเหตุการณ์กลางจุดเดียวpublic interface IOrderMediator{ void Notify(object sender, string ev);}
// component ฐาน: รู้จักแค่ mediator ไม่รู้จัก component ตัวอื่นเลยpublic abstract class OrderComponentBase{ protected readonly IOrderMediator Mediator;
protected OrderComponentBase(IOrderMediator mediator) { Mediator = mediator; }}
// component รับชำระเงินpublic class PaymentComponent : OrderComponentBase{ public PaymentComponent(IOrderMediator mediator) : base(mediator) { }
public void ConfirmPayment() { Console.WriteLine("ยืนยันการชำระเงินเรียบร้อย"); // แจ้ง mediator เท่านั้น ไม่เรียก shipping หรือ notification ตรง ๆ Mediator.Notify(this, "paid"); }}
// component จัดส่งสินค้าpublic class ShippingComponent : OrderComponentBase{ public ShippingComponent(IOrderMediator mediator) : base(mediator) { }
public void StartShipping() { Console.WriteLine("เริ่มจัดส่งสินค้า"); Mediator.Notify(this, "shipped"); }}
// component แจ้งเตือนลูกค้าpublic class NotificationComponent : OrderComponentBase{ public NotificationComponent(IOrderMediator mediator) : base(mediator) { }
public void SendEmail(string message) { Console.WriteLine($"ส่งอีเมลถึงลูกค้า: {message}"); }}
// concrete mediator: รวมตรรกะการประสานงานทั้งหมดของกระบวนการสั่งซื้อไว้ที่เดียวpublic class OrderProcessMediator : IOrderMediator{ private readonly ShippingComponent _shipping; private readonly NotificationComponent _notification;
public OrderProcessMediator() { _shipping = new ShippingComponent(this); _notification = new NotificationComponent(this); }
public PaymentComponent CreatePayment() => new PaymentComponent(this);
public void Notify(object sender, string ev) { switch (ev) { case "paid": // ชำระเงินสำเร็จ -> สั่งให้เริ่มจัดส่งและแจ้งเตือนลูกค้า _shipping.StartShipping(); _notification.SendEmail("คำสั่งซื้อของคุณชำระเงินแล้ว"); break;
case "shipped": // จัดส่งแล้ว -> แจ้งเตือนลูกค้าอีกครั้ง _notification.SendEmail("คำสั่งซื้อของคุณถูกจัดส่งแล้ว"); break; } }}
// การใช้งานvar mediator = new OrderProcessMediator();var payment = mediator.CreatePayment();payment.ConfirmPayment();// PaymentComponent ไม่เคยมี reference ไปยัง ShippingComponent หรือ NotificationComponent เลย// การเพิ่ม component ใหม่ เช่น InventoryComponent ทำได้โดยแก้แค่ OrderProcessMediatorหมายเหตุ: ในโลก .NET หลายคนคุ้นกับ library MediatR ที่ตั้งชื่อว่า “mediator” แต่รูปแบบที่มันใช้จริงคือ command dispatcher / in-process message bus (ส่ง request ไปหา handler เดียวผ่าน generic pipeline) ซึ่งใกล้เคียง Command มากกว่ารูปแบบ GoF Mediator ที่ประสานงาน colleague หลายตัวพร้อมกันตามที่อธิบายในหน้านี้
เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- component หลายตัวมีความสัมพันธ์กันแบบยุ่งเหยิง (n×n) จนแก้ไขหรือทดสอบยาก
- ต้องการนำ component กลับไปใช้ในบริบทอื่น แต่ตอนนี้มันผูกติดกับเพื่อนบ้านมากเกินไป
- ตรรกะการประสานงานระหว่าง object ซับซ้อนและเปลี่ยนบ่อย อยากรวมไว้ที่จุดเดียวเพื่อแก้ง่าย
- กำลังสร้าง UI ที่มี widget จำนวนมาก ต้อง enable/disable/validate ข้ามกันตามสถานะ (คลาสสิกของ dialog box)
- ต้องการ decouple ผู้ส่งเหตุการณ์ออกจากผู้รับ โดยยังคงควบคุม flow แบบ synchronous ได้ชัดเจน (ต่างจาก event-driven ล้วน ๆ)
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- ระบบมี object แค่ 2-3 ตัวที่คุยกันแบบตรงไปตรงมา — เพิ่ม mediator เข้าไปมีแต่จะเพิ่ม indirection โดยไม่จำเป็น
- ตรรกะการประสานงานเรียบง่ายและไม่น่าจะเปลี่ยนบ่อย ใช้การเรียกเมท็อดตรง ๆ ชัดเจนกว่า
- ทีมยังไม่มีวินัยในการคุม scope ของ mediator ให้เล็ก — ความเสี่ยงคือ mediator บวมกลายเป็น God Object ที่รู้ทุกอย่างและแก้ยากกว่าปัญหาที่มันแก้แต่แรก
- เหตุการณ์ในระบบเป็นแบบ many-to-many และไม่จำเป็นต้องรู้ผู้รับล่วงหน้า — กรณีนั้น Event Aggregator หรือ Domain Events อาจเหมาะกว่าเพราะไม่ผูก mediator ให้ต้องรู้จัก component ปลายทางทุกตัว
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | ลด coupling ระหว่าง component ให้เหลือแค่แต่ละตัวรู้จัก mediator เท่านั้น |
| ข้อดี | รวมตรรกะการประสานงานไว้ที่จุดเดียว อ่านและแก้ไข flow ทั้งหมดได้จากที่เดียว |
| ข้อดี | เพิ่ม component ใหม่หรือเปลี่ยนกติกาการสื่อสารได้โดยแตะแค่ mediator ไม่ต้องไล่แก้ทุกตัว |
| ข้อดี | ทดสอบ component แต่ละตัวแยกกันได้ง่ายขึ้น เพราะไม่มี dependency ไขว้กันไปมา |
| ข้อเสีย | mediator เสี่ยงบวมเป็น God Object ที่รู้ทุกอย่างและถือ business logic มากเกินไป |
| ข้อเสีย | เพิ่ม indirection หนึ่งชั้น — ตามรอย control flow จากปลายหนึ่งไปอีกปลายทำได้ยากขึ้นกว่าการเรียกตรง |
| ข้อเสีย | อาจกลายเป็นคอขวด (bottleneck) ของระบบถ้าตรรกะการประสานงานซับซ้อนขึ้นเรื่อย ๆ โดยไม่แตกย่อย |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Command — encapsulate การกระทำเป็น object; mediator มักใช้ command ภายในเพื่อสั่งให้ colleague ทำงาน
- Observer — แจกจ่ายเหตุการณ์แบบ one-to-many; mediator ต่างตรงที่รู้จักและควบคุม colleague ทุกตัวโดยตรง ไม่ใช่แค่ broadcast
- Facade — ให้ interface เดียวเพื่อเรียก subsystem ที่ซับซ้อน; facade ทางเดียว ส่วน mediator ประสานงานสองทางระหว่างหลาย object
- Event Aggregator — ตัวกลางแบบ publish/subscribe ที่ผู้ส่งและผู้รับไม่ต้องรู้จักกันเลยแม้แต่ผ่าน mediator
- Domain Events — เหตุการณ์ระดับ domain ที่กระจายผลกระทบข้าม aggregate โดยไม่ผูกกับ mediator ตัวใดตัวหนึ่ง
- The Law of Demeter — หลักการเบื้องหลัง Mediator: object ควรคุยกับ “เพื่อนใกล้ตัว” เท่านั้น ไม่ไล่เรียก object ที่อยู่ลึกในโครงสร้าง