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
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”// 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