設計模式學習總結:觀察者模式(Observer Pattern)
意圖
定義物件間的一種一對多的依賴關係,當一個物件的狀態發生變化時,所以依賴於它的物件都得到通知並被自動更新。
適用性
- 當一個抽象物件有兩個方面,其中一個方面依賴於另一個方面。將這兩者封裝在兩個獨立的物件中使它們可以被獨立地改變與複用。
- 當對一個物件的改變同時需要改變其他物件,而不知道具體有多少物件有待改變時。
- 當一個物件必須通知其他物件而它又不能假定其他物件是誰時。
結構
Subject為觀察目標,觀察主題;Observer為觀察者。
優缺點
優點
1.目標與觀察者之間是鬆耦合的(抽象耦合),一個目標所知道的僅僅是它有一系列觀察者,每個觀察者都符合抽象的Observer類的簡單介面,這樣目標與觀察者的耦合是抽象和最小的。因此它們可以屬於一個系統中的不同抽象層次,一個處於低層次的目標物件可與一個處於較高層次的觀察者並通知它,這樣就保持了系統層次的完整性。
class Observer;
class Subject
{
public:
void registerObserver(Observer *observer);
void removeObserver(Observer *observer);
void notifyObservers();
}
class Observer
{
void update(Subject *subject);
}
class ConcreteSubject : public Subject
{
...
}
class ConcreteObserver : public Observer
{
...
}
auto observer = new Observer();
auto subject = new Subject();
auto concreteObserver = new ConcreteObserver();
auto concreteSubject = new ConcreteSubject();
//不同抽象層次間可以進行相互通訊,只要它們遵循相應的介面,這就是鬆耦合
subject->registerObserver(observer);
subject->registerObserver(concreteObserver);
concreteSubject ->registerObserver(observer);
concreteSubject ->registerObserver(concreteObserver);
2.支援廣播通訊。目標物件不關心到底有多少物件對自己感興趣,它唯一的責任就是通知它的各觀察者,這給了你在任何時候增加和刪除觀察者的自由。處理還是忽略一個通知取決於觀察者。
缺點
可能引起意外的更新。因為觀察者並不知道其他觀察者的存在,在目標上一個看似無害的操作可能引起一系列對觀察者或者依賴這些觀察者物件的更新,如果依賴準則的定義或維護不當,常常會引起錯誤的更新,這種錯誤通常難以察覺。
實現
1> 一個目標物件跟蹤它應通知的觀察者的最簡單的方法是顯式地在目標中儲存它們的引用。然而,當目標很多而觀察者較少時,這樣儲存可能代價太高。一個解決方法是用時間換空間,用一個關聯查詢機制來維護目標到觀察者的對映。這樣一個沒有觀察者的目標就沒有儲存開銷,但是其增加了訪問觀察者的開銷。
2> 觀察多個目標。有些情況下一個觀察者依賴於多個目標是有意義的,在這種情況下,必須擴充套件Update介面使觀察者知道哪一個目標送來的通知。目標物件可以簡單的將自己作為Update操作的一個引數,讓觀察者知道去檢查哪一個目標。
3> 誰觸發更新。有兩種選擇:(a)由目標物件的狀態設定操作在改變目標物件狀態後自動呼叫Notify。這種方法的優點是客戶不需要記住要在目標物件上呼叫Notify,缺點是多個連續的操作會產生多次連續的更新,可能效率過低。(b)由客戶在適當的時間呼叫Notify,這樣可以避免不必要的連續更新,缺點是給客戶增加了觸發更新的責任,較易出錯。
4> 對已刪除目標的懸掛引用。刪除一個目標時應注意不要在其觀察者中遺留對該目標的懸掛引用。當刪除一個目標時,應該讓它的觀察者對該目標的引用復位。另外,在刪除觀察者時,也要將目標中的觀察者移除。
class Observer;
class Subject
{
public:
~Subject()
{
//析構時刪除自己的所有觀察者
for(auto observer in _observers)
{
removeObserver(observer);
}
}
void registerObserver(Observer *observer)
{
_observers.push_back(observer);
observer->setSubject(this);
}
void removeObserver(Observer *observer);
{
//刪除某觀察者時將觀察者中對自己的引用清零
_observers.earse(observer);
observer->setSubject(nullptr);
}
void notifyObservers();
private:
std::list<Observer *> _observers;
}
class Observer
{
public:
Observer(Subject *subject)
~Observer()
{
//在析構時要通知其觀察目標刪除自己
if(_subject != nullptr)
_subject->removeObserver(this);
}
void setSubject(Subject *subject){_subject = subject;}
void update(Subject *subject);
private:
Subject *_subject = nullptr;
}
5> 在發出通知前確保目標的狀態自身是一致的。當Subject的子類呼叫繼承的這項操作時,很容易無意中違反這條準則,如下:
class Subject
{
public:
void registerObserver(Observer *observer);
void removeObserver(Observer *observer);
void notify();
virtual void operation(int newValue)
{
notify();
}
private:
int _myValue;
}
class MySubject : public subject
{
void operation(newValue)
{
//已經觸發了notify
Subject::operation(int newValue);
//觸發notify之後才更新狀態
_myValue += newValue;
}
}
可以用模板方法來避免這種錯誤。定義子類可以重定義的原語操作,並將notify作為模板方法的最後一個操作。
class Sunject
{
public:
void notify();
virtual void firstOperation();
virtual void secondOperation();
void operation()
{
firstOperation();
secondOperation();
notify();
}
}
6>避免特定於觀察者的更新協議–推/拉模型。推模型是目標向觀察者傳送所有改變的資訊;拉模型是目標除了最小通知外什麼資料也不送出,而是由觀察者顯式向目標進行細節查詢。推模型可能使得觀察者相對難以複用,而拉模型可能效率較差。
7>顯式地指定感興趣的改變。你可以擴充套件目標的註冊介面,讓觀察者註冊為僅對特定事件感興趣,從而提高更新的效率。
void Subject::registerObserver(Observer *observer, Aspect &aspect);
void Observer::Update(Subject *subject, Aspect &aspect);
8>封裝複雜的更新語義。當目標與觀察者間的依賴關係特別複雜時,可能需要一個維護這些關係的物件,我們將其稱為更改管理器(ChangeManager),它的目的是儘量減少觀察者反應其目標的狀態變化所需的工作量。ChangerManager有三個職責:
- 它將一個目標對映到它的觀察者並提供了一個介面來維護這個對映,這就不需要由目標來維護對其觀察者的引用,反之亦然。
- 它定義了一個特定的更新策略
- 根據一個目標的請求,它更新所有依賴於這個目標的觀測者。
當一個觀察者觀察多個目標時DAGChangeManager要更好用一些,它可以保證觀察者僅接受一個更新,而不會接受到多個冗餘的更新。當不存在多重更新時,SimpleChangeManager更好一些。
10>用不支援多重繼承的語言(如Smalltalk,Lua)時通常不單獨定義Subject和Observer類,而是將它們的介面結合到一個類中,這允許你定義一個又是目標又是觀察者的物件而不需要多重繼承。
例項
相關模式
ChangeManager是一個Mediator(中介者)模式的例項,且其通常是一個Singleleton(單例)。