用Delphi实现观察者模式(Observer模式)

http://delphi.sqlitedeveloper.com/Delphi/delphi_observer.htm

用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实现了数据感知组件的自动同步功能。

 

用Delphi实现观察者模式(Observer模式)

上一篇:我不建议在C#中用下划线_开头来表示私有字段


下一篇:delphi try except与try finally语句用法以及区别