設計模式之「Observer」註疏#01

原文首發於我的微信公眾號:GeekArtT.

Observer設計模式是為了解決「信息同步更新」的問題而存在的。它試圖解決這樣一個問題:如果有「一堆對象」都跟隨「某一對象」的變化而變化,那麼,如何能夠保持「這堆對象」能夠同步更新呢?特別是,「這堆對象」很可能在運行時(run-time)不斷被添加或者被刪除,你設計的機制該如何既能保持增添/刪除的靈活性,而又能使「當前這堆對象」同步更新呢?

僅僅是抽象地看待上述這個問題,或許很難有思路。解決方案的形成可以依託於生活中具體業務場景的類比。

很多書籍介紹觀察者(Observer)這個設計模式喜歡運用「內容訂閱者」和「發布信息平台」的對應關係。這個比喻從生產流程上講,完全符合,但是作為比喻,還是顯得有些生硬。我認為,一個更好的、更富有啟發性的類比是「品牌商店」與「旗下加盟商/分店」在價格上的依存關係。好不誇張的說,一旦做出這個類比,解決方案就會自然浮現。

例如,McDonald在一座城市開店,其價格都是高度統一。如果某一個分店的價格低於別的分店,就會打亂跨國大公司的整體戰略部署,是絕不允許的。所以,一個核心的目標是,每一個分店的價格都必須時刻同步統一。另一方面,每個地區的分店,都有可能隨著市場的反饋而做出調整:或者因為銷量過低而關閉,或者因為某個新的商圈的出現而增加分店。那麼,在分店的數目、位置都在隨時變動的情形下,應該用一套什麼機制才能保證分布在城市各地的分店都使用統一的價格呢?

一旦有了恰當的比喻,問題自然迎刃而解。保持分店的價格有什麼困難的?

在總店的通訊本上,記錄下各個分店的電話號碼。一旦有了新的價格策略,立馬根據通訊本上記錄的電話號碼,撥打各個分店的電話,通知各個分店。如果建立了新的分店,就在總店的通訊本上添加新分店的電話號碼;如果關閉了一家分店,就在總店通訊本上去掉相應分店的電話號碼。由此,各分店的價格便能夠同步統一,且保持相對好的靈活性。

有了這段生活場景作為指導綱領,我們便可以寫出具體的Observer設計模式的代碼了。考慮到GoF(Design Patterns)這本書的巨大影響,我首先展示如何運用上述類比一步步寫出GoF中Observer設計模式這一節的示例代碼。

在GoF中,示例代碼的應用場景是:如果我有一個class能夠實時獲取當前時間,但是,我又想用兩種方式Digital Clock和Analog Clock的方式去做展示(可以想像為`10:28`和`10h28min`這樣不同的展示方式)。

首先設計「分店」類。它最主要的功能就是根據「總店」通報的信息,將自己的信息更新。對應GoF的C++代碼,Subject類充當了「總店」的角色。

class Subject;nnclass Observer {npublic:n virtual ~Observer();n virtual void Update(Subject* theChangedSubject) = 0;nprotected:n Observer();n}n

這裡Update()方法是需要有Subject類作為參數的。因為,作為分店,自己是不可以任意地改動價格的,你有且只能根據「總店」的消息去做變更,別的數據源都是禁止的。

然後設計「總店」類,用它來衍生出「能夠實時獲取當前時間的類」。按照之前的思路,這個類除了必要的構造/析構函數外:

  • 需要有一個「通訊本」記錄各分店的電話號碼。
  • 相應地,應該具備添加/刪除電話號碼的方法。
  • 另外,還應當有一個方法,能夠根據通訊本上記錄的信息撥打各個分店的電話號碼。這一行為抽象地說,即是能夠根據一個信息記錄本,來逐一通知各個「分店」進行信息更新。

GoF中的C++的代碼如下:

class Subject {npublic:n virtual ~Subject();nn virtual void Attach(Observer *);n virtual void Detach(Observer *);n virtual void Notify();nprotected:n Subject();nprivate:n List<Observer*> *_observers;n};n

這裡,列表_observers充當了通訊本的角色,用它來保存各個「分店」的指針。本來,指針即是變數的門牌號。把「分店」的這個號碼記錄在通訊本上,就能夠任意地訪問「分店」。從這點上來說,它比僅僅記錄的電話號碼還要強大有用。

Attach()Detach()方法自然就是用於添加/刪除這個通訊本的信息了。而Notify()即是字面意思所表述的,用於通知各個分店「價格有變」。而通知的目的,實質是讓各分店做更新,所以,這裡作為父類的Notify()是有統一的更新方法:

void Subject::Notify() {n ListIterator<Observer*> i(_observers);nn for(i.First(); !i.IsDone(); i.Next()) {n i.CurrentItem()->Update(this);n }n}n

這裡所用的方法,即是依次將通訊本上的分店調取出來,讓它們各自調用自己的Update()方法,並將自己,this,作為參數傳入。告知分店把自己作為信息源來進行同步更新。

構造完父類,下面可以構建具體的時鐘類。首先是能夠實時獲取時間的「總店」類:

class ClockTimer : public Subject {npublic:n ClockTimer();nn virtual int GetHour();n virtual int GetMinute();n virtual int GetSecond();nn void Tick();n}nnvoid ClockTimer::Tick() {n // update internal time-keeping staten // ...n Notify();n}n

這裡,Tick()方法用於定時抓取最新的時間狀態信息。

然後是第一個「分店」類:

class DigitalClock: public Widget, public Observer {nprivate:n ClockTimer* _subject;npublic:n DigitalClock(ClockTimer*);n virtual ~DigitalClock();nn virtual void Update(Subject*);n // overrides Observer operationnn virtual void Draw();n // overrides Widget operation;n // defines how to draw the digital clockn};nnDigitalClock::DigitalClock(ClockTimer* s) {n _subject = s;n _subject->Attach(this);n}nnDigitalClock::~DigitalClock() {n _subject->Detach(this);n}n

這個類裡面,運用了private變數_subject去保存它的數據源,並在構造函數中將其初始化。之後便可以運用這個_subject去核對Update是否是被正確的數據源通知,以此來作為更新與否的標準:

void DigitalClock::Update(Subject* theChangedSubject) {n if(theChangedSubject == _subject) {n Draw();n }n}nnvoid DigitalClock::Draw() {n // get the new values from the subjectn int hour = _subject->GetHour();n int minute = _subject->GetMinute();n // etc.nn // draw the digital clockn}n

之後是第二個「分店」AnalogClock的構建:

class AnalogClock : public Widget, public Observer {nprivate:n ClockTimer* _subject;npublic:n AnalogClock(ClockTimer*);n virtual ~AnalogClock();nn virtual void Update(Subject*);n virtual void Draw();n // ...n};nn// ...n

構建完了這三大部分,便可以直接使用上面完成的Observer模式了:

ClockTimer* timer = new ClockTimer;nnAnalogClock* analogClock = new AnalogClock(timer);nDigitalClock* digitalClock = new DigitalClock(timer);n

這樣,每當timer中的Tick()方法抓取到了新的時間信息,兩個Clock就會得到及時更新,並作出相應地不同展示。

如果你喜歡我的文章或分享,請長按下面的二維碼關注我的微信公眾號,謝謝!


推薦閱讀:

MVC 架構與 Observer 模式有什麼異同點?
C++模版類如何動態獲取類型?
敏捷開發中如何保證界面設計?
通俗 Python 設計模式——原型設計模式
設計模式之組合模式

TAG:设计模式 | 软件工程 |