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

Observer

subject แจ้ง observer ทุก​ตัว​เมื่อ​สถานะ​เปลี่ยน

ปัญหา​ที่ Observer แก้​คือ: เมื่อ object หนึ่ง (เรียก​ว่า subject หรือ publisher) เปลี่ยน​สถานะ มัก​มี object อื่น​อีก​หลาย​ตัว​ที่​ต้อง “รู้” เรื่อง​นี้​และ​ตอบ​สนอง​ตาม แนวทาง​ที่​ไม่​ดี​มี​สอง​แบบ คือ (1) ให้ subject ผูก reference ไป​ยัง object เหล่า​นั้น​ตรง ๆ ซึ่ง​ทำให้ subject รู้​รายละเอียด​ของ​ทุก​ฝ่าย​ที่​สนใจ — ผิด Open/Closed Principle เพราะ​เพิ่ม​ผู้​สนใจ​ใหม่​ที​ไร​ต้อง​แก้ subject หรือ (2) ให้ object อื่น​คอย poll ถาม​สถานะ​ซ้ำ ๆ ซึ่ง​สิ้น​เปลือง​และ​มี latency

Observer แก้​ด้วย​การ​ให้ subject เก็บ รายชื่อ observer ผ่าน interface กลาง (เช่น IObserver.Update()) เท่านั้น ไม่รู้จัก concrete type ของ observer เลย เมื่อ​สถานะ​เปลี่ยน subject แค่​วน​เรียก Update() ให้ observer ทุก​ตัว​ใน​รายชื่อ — จบ​หน้าที่ ส่วน observer แต่ละ​ตัว​ตัดสิน​ใจ​เอง​ว่า​จะ​ทำ​อะไร​กับ​การ​แจ้ง​เตือน​นั้น นี่​คือ​แก่น​ของ one-to-many dependency ที่ loose coupling ระหว่าง​ฝั่ง​ที่​เปลี่ยน​สถานะ​กับ​ฝั่ง​ที่​ตอบ​สนอง — เพิ่ม observer ใหม่​ได้​โดย​ไม่​แตะ code subject เลย ตรง​กับ​หลัก OCP

classDiagram
    class Subject {
      -List~IObserver~ observers
      +Attach(IObserver o)
      +Detach(IObserver o)
      +Notify()
    }
    class IObserver {
      <<interface>>
      +Update(Subject s)
    }
    class ConcreteSubject {
      -state
      +GetState()
      +SetState(v)
    }
    class ConcreteObserverA {
      +Update(Subject s)
    }
    class ConcreteObserverB {
      +Update(Subject s)
    }
    Subject o-- IObserver
    Subject <|-- ConcreteSubject
    IObserver <|.. ConcreteObserverA
    IObserver <|.. ConcreteObserverB
  • Subject เก็บ collection ของ IObserver และ​มี Attach/Detach ให้​สมัคร-เลิก​สมัคร กับ Notify ที่​วน​แจ้ง​ทุก​ตัว
  • IObserver เป็น interface กลาง​ที่ subject รู้จัก​แค่​นี้ — ไม่รู้ concrete type
  • ConcreteSubject ถือ state จริง​และ​เรียก Notify() เมื่อ state เปลี่ยน
  • ConcreteObserver implement Update() ตาม​ที่​ตัวเอง​ต้องการ​ตอบ​สนอง เช่น อัปเดต UI, log, ส่ง email

ลำดับ​เหตุการณ์​ตรง​ไป​ตรง​มา: observer สมัคร​สมาชิก​กับ subject ผ่าน Attach() เมื่อ​สถานะ​ของ subject เปลี่ยน (เช่น​ถูก​เรียก SetState()) subject จะ​เรียก Notify() ซึ่ง​วน loop เรียก Update() ของ observer ทุก​ตัว​ที่​อยู่​ใน​รายชื่อ — ที​ละ​ตัว​และ synchronous โดย default (observer ตัว​ถัด​ไป​จะ​ไม่​ถูก​เรียก​จนกว่า Update() ของ​ตัว​ก่อนหน้า​จะ return)

sequenceDiagram
    participant Client
    participant Subject
    participant ObserverA
    participant ObserverB

    Client->>Subject: Attach(ObserverA)
    Client->>Subject: Attach(ObserverB)
    Client->>Subject: SetState(newValue)
    Subject->>Subject: Notify()
    Subject->>ObserverA: Update(this)
    ObserverA-->>Subject: return
    Subject->>ObserverB: Update(this)
    ObserverB-->>Subject: return

มี​สอง​รูปแบบ​การ​ส่ง​ข้อมูล​ใน Update(): push model ที่ subject ส่ง state ที่​เปลี่ยน​ไป​ให้ observer โดยตรง (เช่น Update(decimal newPrice)) กับ pull model ที่ subject ส่ง​แค่​ตัวเอง (Update(Subject s)) แล้ว​ให้ observer เรียก getter กลับ​ไป​ดึง​เฉพาะ​ข้อมูล​ที่​ต้องการ​เอง — pull model ยืดหยุ่น​กว่า​เพราะ observer แต่ละ​ตัว​ต้องการ​ข้อมูล​ไม่​เท่า​กัน แต่​เพิ่ม coupling ทาง type เล็กน้อย​เพราะ​ต้อง​รู้จัก type ของ subject

ข้อ​ควร​ระวัง​เชิง​ปฏิบัติ: ลำดับ​ที่ observer ถูก​แจ้ง​เตือน​มัก​ไม่​รับประกัน (ขึ้น​กับ​ลำดับ​ใน collection) และ​ถ้า Update() ของ observer ตัว​ใด​ตัว1 throw exception หรือ​ช้า จะ​กระทบ observer ตัว​ถัด​ไป​ด้วย เพราะ​เป็น synchronous call chain — ถ้า​ต้องการ decouple มากกว่า​นี้ (เช่น async, มี broker กลาง) มัก​ยก​ระดับ​ไป​ใช้ Event Aggregator หรือ Domain Events

// Observer interface — subject รู้จักแค่ contract นี้
public interface IStockObserver
{
void Update(Stock stock);
}
// Subject — เก็บรายชื่อ observer และแจ้งเตือนเมื่อราคาหุ้นเปลี่ยน
public class Stock
{
private readonly List<IStockObserver> _observers = new();
public string Symbol { get; }
public decimal Price { get; private set; }
public Stock(string symbol, decimal initialPrice)
{
Symbol = symbol;
Price = initialPrice;
}
public void Attach(IStockObserver observer) => _observers.Add(observer);
public void Detach(IStockObserver observer) => _observers.Remove(observer);
public void SetPrice(decimal newPrice)
{
if (newPrice == Price) return; // ไม่เปลี่ยน ไม่ต้องแจ้ง
Price = newPrice;
Notify(); // แจ้ง observer ทุกตัวหลังสถานะเปลี่ยนจริง
}
private void Notify()
{
// ก็อปปี้ list ก่อนวน กันปัญหา observer แก้ list ระหว่าง iterate
foreach (var observer in _observers.ToList())
{
observer.Update(this); // pull model — observer ดึงข้อมูลจาก this เอง
}
}
}
// ConcreteObserver #1 — แสดงผลบนหน้าจอ
public class PriceDisplay : IStockObserver
{
public void Update(Stock stock)
{
Console.WriteLine($"[Display] {stock.Symbol} ราคาปัจจุบัน: {stock.Price:C}");
}
}
// ConcreteObserver #2 — แจ้งเตือนเมื่อราคาต่ำกว่าเกณฑ์ที่กำหนด
public class PriceAlert : IStockObserver
{
private readonly decimal _threshold;
public PriceAlert(decimal threshold) => _threshold = threshold;
public void Update(Stock stock)
{
if (stock.Price < _threshold)
Console.WriteLine($"[Alert] {stock.Symbol} ต่ำกว่า {_threshold:C} แล้ว!");
}
}
// การใช้งาน
var stock = new Stock("AAPL", 150m);
var display = new PriceDisplay();
var alert = new PriceAlert(140m);
stock.Attach(display);
stock.Attach(alert);
stock.SetPrice(145m); // แจ้งทั้งสองตัว
stock.SetPrice(135m); // แจ้งทั้งสองตัว — คราวนี้ alert จะเตือนด้วย
stock.Detach(alert); // เลิกสมัครได้ทุกเมื่อ
stock.SetPrice(130m); // มีแค่ display ที่ได้รับแจ้ง

C# ยัง​มี built-in support ผ่าน IObservable<T> และ IObserver<T> (ใน System) ซึ่ง​เป็น​รูปแบบ Observer ที่ formalize แล้ว รวม​ถึง event/delegate ของ​ภาษา​ก็​เป็น implementation ของ​แนวคิด​นี้​ใน​ระดับ​ภาษา​เช่น​กัน

  • เมื่อ​การ​เปลี่ยน​สถานะ​ของ object หนึ่ง​ต้อง​กระตุ้น​ให้ object อื่น​จำนวน​หนึ่ง (ที่​ไม่รู้​จำนวน​แน่นอน​ล่วงหน้า) ทำงาน​ตาม
  • เมื่อ​ต้องการ​เพิ่ม/ลด “ผู้​ฟัง” แบบ dynamic ที่ runtime โดย​ไม่​แก้ code ฝั่ง subject
  • ระบบ event-driven, GUI framework (ปุ่ม​กด, การ​เปลี่ยน​ค่า​ใน form) หรือ real-time dashboard
  • เมื่อ​ต้องการ decouple ฝั่ง​ที่​ผลิต event ออก​จาก​ฝั่ง​ที่​ตอบ​สนอง event ตาม​หลัก SoC
  • เมื่อ​มี observer แค่​ตัว​เดียว​แบบ​ตายตัว​และ​ไม่มี​แนวโน้ม​จะ​เพิ่ม — เรียก method ตรง ๆ ง่าย​กว่า​และ​อ่าน​ง่าย​กว่า
  • เมื่อ​จำนวน publisher/subscriber มาก​และ​ซับซ้อน​จน​ต้อง​มี routing, filtering หรือ async delivery ข้าม process — ควร​ใช้ message broker หรือ​ยก​ระดับ​ไป​เป็น Event Aggregator แทน
  • เมื่อ observer ต้อง​ประมวล​ผล​ตาม​ลำดับ​ที่​รับประกัน​ได้​ชัดเจน หรือ error ของ observer หนึ่ง​ต้อง​ไม่​กระทบ​ตัว​อื่น — synchronous notify แบบ​พื้นฐาน​ไม่​ได้​ออกแบบ​มา​เพื่อ​สิ่ง​นี้
  • เมื่อ​ความ​สัมพันธ์ subject-observer อาจ​ก่อ memory leak ได้​ง่าย (observer ที่​ลืม Detach() จะ​ถูก subject ยึด reference ไว้​ตลอด)
ด้านรายละเอียด
ข้อดีรองรับ OCP — เพิ่ม observer ใหม่​ได้​โดย​ไม่​แก้ subject
ข้อดีLoose coupling ระหว่าง subject กับ observer ผ่าน interface กลาง
ข้อดีสร้าง​ความ​สัมพันธ์​ระหว่าง object ได้​ตอน runtime อย่าง​ยืดหยุ่น
ข้อ​เสียลำดับ​การ​แจ้ง​เตือน observer มัก​ไม่​รับประกัน อาจ​สร้าง bug เชิง dependency ระหว่าง observer
ข้อ​เสียDebug ยาก​ขึ้น เพราะ flow การ​ทำงาน​กระจาย​ไป​ตาม observer หลาย​ตัว​ที่ subject ไม่รู้จัก
ข้อ​เสียเสี่ยง memory leak ถ้า observer ไม่ Detach() ตัวเอง​เมื่อ​ไม่​ใช้​แล้ว (โดย​เฉพาะ​ใน GUI/long-lived subject)
  • Event Aggregator — ยก​ระดับ Observer เมื่อ publisher/subscriber มี​จำนวน​มาก ต้องการ decouple ผ่าน broker กลาง
  • Mediator — decouple การ​สื่อสาร​ระหว่าง object กลุ่ม​หนึ่ง​ผ่าน​ตัวกลาง คล้าย Event Aggregator แต่​เน้น​ความ​สัมพันธ์​แบบ​สอง​ทาง
  • Domain Events — ใช้​แนวคิด Observer ใน​ระดับ domain model เพื่อ​สื่อสาร​ระหว่าง aggregate โดย​ไม่​ผูก coupling ตรง
  • Open/Closed Principle — หลักการ​ที่ Observer ช่วย​ให้​ทำได้​จริง​ใน​เรื่อง​การ​ขยาย subscriber
  • Separation of Concerns — เหตุผล​เชิง​หลักการ​ที่​ทำให้​แยก subject ออก​จาก logic ของ observer