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

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 ที่​เกี่ยวข้อง

ตัวอย่าง​ขยาย​จาก​กระบวนการ​สั่ง​ซื้อ​สินค้า: เมื่อ​ชำระ​เงิน​สำเร็จ ต้อง​เริ่ม​จัด​ส่ง​และ​แจ้ง​เตือน​ลูกค้า — โดยที่ 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 ที่​อยู่​ลึก​ใน​โครงสร้าง