天天看點

用Delphi實作觀察者模式(Observer模式)

有一段時間我對IE程式設計非常感興趣,于是就在Yahoo加入了一個IE程式設計的興趣小組,隻要有人在興趣小組中提出或者回答了一個問題,釋出的資訊就會發送給所有興趣小組的注冊使用者,這種模式實際上就是釋出-訂閱模式,又稱觀察者模式。

觀察者模式中有兩個角色,其中一個是目标,另外一個是觀察者,對于興趣小組來說,注冊使用者就是觀察者,而興趣小組本身是目标。興趣小組必須提供注冊的機制,這樣興趣小組才能知道使用者的郵件位址,可以維護一個訂戶的郵件清單,能在資訊更新時向訂戶發送變更通知。同時興趣小組必須提供登出的機制,當使用者對目标不感興趣時,可以取消訂閱,從我訂閱興趣小組的經驗來看,登出機制是非常必要的,因為IE興趣小組是一個名副其實的郵箱轟炸者,每當我出差幾天回家之後,就會發現郵箱中多了上百封信,其中通常還會有263的郵箱超容警告信。使用注冊和登出機制可以動态增加或删除觀察者。

觀察者模式的特點在于觀察者依賴于目标,同時目标通常隻有一個,而觀察者可以有多個,兩者之間是1對多的關系,目标狀态的變更通常是由觀察者對目标狀态的修改導緻的,下面是觀察者模式的UML圖:

用Delphi實作觀察者模式(Observer模式)

上面目标的Register和UnRegister方法用來注冊和登出觀察者,而SetState和GetState方法被觀察者用來設定或者擷取目标狀态的,目标的Notify方法用來周遊觀察者清單來調用觀察者的Update方法通知觀察者目标發生了變化。下面是觀察者模式的時序圖:

用Delphi實作觀察者模式(Observer模式)

每次當一個觀察者修改了目标狀态後,會觸發目标的Update方法,然後目标在Update方法中周遊觀察者清單,然後按順序調用觀察者的Update方法通知觀察者目标發生了變化,觀察者則在目标發生變化後,調用目标的GetState方法來獲得更新後的目标的狀态。

要注意的上面的觀察者模式實際上是一種采用了拉模式來進行更新,每次目标變化後,隻是通知觀察者有變化,觀察者需要主動的調用目标的GetState方法來獲得變更的狀态,還有一種方式是直接在Update方法中将變更的目标的狀态作為參數傳給觀察者,這種模式叫推模式。

當目标不清楚它的觀察者的細節時,可以使用拉模式,而當目标對觀察者的一些資訊清楚時,可以考慮推模式,這樣效率高些,但是推模式相對來說,不容易複用,因為這要求目标了解更多的觀察者的資訊,造成緊偶合。

使用觀察者模式的好處

使用觀察者模式的主要好處就是減少了觀察者之間的偶合,每個觀察者隻需要知道目标就可以了,無須關心其它觀察者。考慮一下如果不使用觀察者模式,對于興趣小組這樣一個應用來說,每個觀察者都需要知道其它觀察者的郵件位址,然後每次發送資訊時,都要給每個使用者發送資訊,兩者的比較見下圖:

用Delphi實作觀察者模式(Observer模式)
用Delphi實作觀察者模式(Observer模式)

可以看到不使用觀察者模式的話,觀察者之間的關聯非常多,對于4個人來說,有6條關聯,而使用郵件清單之後,關聯變成了4條,同時觀察者互相之間不需要了解,如果有上百人的話,前一種方式的關聯會成階乘方式增加,而郵件清單方式,關聯是成線性增加,系統的偶合明顯要少的多,同時從系統變化來看,當在左邊圖中增加一個新的觀察者時,它需要知道其它四個觀察者,而在右邊的模型中,增加一個觀察者,它隻需要知道郵件清單就可以了,顯然更有利于擴充。

VCL中的觀察者模式

Delphi最大的用途就是編寫資料庫程式了,而在VCL的資料感覺控件中就使用了觀察者模式,下面我們來編寫簡單的程式,建立一個項目,然後在窗體上放上一個TTable和TDataSource,設定TTable的DataBaseName屬性為DBDemos,TableName為animals.dbf。然後設定DataSource的DataSet為TTable。接下來放上一個DBGrid和DBImage以及一些DBEdit,将這些DBAware元件同資料源綁定,運作後,在DBGrid中資料記錄中導航後,你會發現,當DBGrid中的目前記錄改變時,其它DBEdit的資料元件的資料也發生了變化,完全同步,這是因為DataSource就相當于觀察者模式中的目标,而DBAware控件則相當于觀察者,它們的關系如下圖示意:

用Delphi實作觀察者模式(Observer模式)

在DBAware元件設定DataSource屬性時,會調用元件的SetDataSource方法:

procedure TDBEdit.SetDataSource(Value: TDataSource);           
begin           
  if not (FDataLink.DataSourceFixed and (csLoading in ComponentState)) then           
    FDataLink.DataSource := Value;           
  if Value <> nil then Value.FreeNotification(Self);           
end;           

方法設定FDataLink.DataSource為目前的DataSource,而對TDataLink的DataSource的指派又會觸發TDataLink的SetDataSource方法的調用:

procedure TDataLink.SetDataSource(ADataSource: TDataSource);           
begin           
  if FDataSource <> ADataSource then           
  begin           
    if FDataSourceFixed then DatabaseError(SDataSourceChange, FDataSource);           
    if FDataSource <> nil then FDataSource.RemoveDataLink(Self);           
    if ADataSource <> nil then ADataSource.AddDataLink(Self);           
  end;           
end;           

可以看到在TDataLink的SetDataSource方法中,調用了DataSource的AddDataLink方法将自身注冊到了DataSource的觀察者清單中,當資料發生變化時,DataSource會調用自身的NotifyDataLinks方法周遊注冊清單FDataLinks中的所有TDataLink,通過調用TDataLink的DataEvent方法通知資料感覺元件資料發生了變化:

procedure TDataSource.NotifyDataLinks(Event: TDataEvent; Info: Longint);           
begin           
  { Notify non-visual links (i.e. details), before visual controls }           
  NotifyLinkTypes(Event, Info, False);           
  NotifyLinkTypes(Event, Info, True);           
end;           
procedure TDataSource.NotifyLinkTypes(Event: TDataEvent; Info: Longint;           
  LinkType: Boolean);           
var           
  I: Integer;           
begin           
  for I := FDataLinks.Count - 1 downto 0 do           
    with TDataLink(FDataLinks[I]) do           
      if LinkType = VisualControl then           
        DataEvent(Event, Info);           
end;           

正是通過觀察者模式,Delphi實作了資料感覺元件的自動同步功能。

繼續閱讀