Simba

博客园 首页 新随笔 联系 订阅 管理

  观察者模式(Observer)定义了对象之间的一对多的依赖关系,当一个对象改变时,他的所有依赖对象都会得到通知并自动更新。观察者模式大体上由两部分组成: 观察者(Observer)和主题(Subject),观察者需要像主题订阅事件,然后当某事件触发时观察者才会得到通知,打个比方,如果我们每天起床后想看看今天的新闻,那么在移动或联通定个手机报,然后每天早上你都会受到一条新闻彩信。

 

观察者模式的应用非常广泛,尤其在C++/C#/Java等面向对象的代码里,经常可以看到这样的接口或函数

addListener(…)、  setListener(...)、  register(…)、  subscribe(…)

  

同样的,也会有很多对应的会有一些

removeListener(…)、 setListener(null)、 unSubscribe(…)

 

等函数成对出现,还有很多类似notify(…)onXXXEvent()之类的函数。

[单从字面上看addListenersetListener有什么区别,什么情况下用addListener()什么情况下用setListener()? 答案是: addListener是在主题可以包含多个观察者的时候,需要提供removeListener(…)函数,而setListener()是主体只有一个观察者的情况,不需要提供额外的remove的函数,如果要删除观察者,只需要调用setListener(NULL)就行,当然这种区别不是必须的,只是这样的代码风格更好而已)

 

在观察者模式中,ObserverSubject获取数据的途径有两种,一种叫做Push()、一种叫做Pull()Push就是Subject主动把数据发送到Observer中,缺点是会发送Observer根本不关心的数据,浪费资源,Pull是某事件发生时Subject通知Observer,然后Observer通过Subject的接口取其所需的数据,可想而知,Observer需要知道Subject的内部结构,这样就破坏了Subject的隐私,即封装性。单从描述上可能无法体会这种模式的内涵,本文后面会实现关于这两种方式的使用,以使读者能有个教清晰的认识。

 

: 如果想系统的学习设计模式而不是仅仅为了应付一次面试或者回答一个问题,那么建议读<Head First Design Pattern>,没有这本书的知识储备,不建议直接看四人帮的设计模式,中高级程序员除外。

 

现在假设要实现一个车载电子监视系统AutoCentralMonitor,仪表盘车速表Speed(通常有机械指针表和电子时速表两种)实时显示当前的行车速度,当行车速度过快达到120KM/H时车内报警器Alarm会发出警告声提示司机注意行车安全。实现这个方案,最简单的设计UML类图如下:

 

图 1 

 

这种设计的伪代码实现如下:

ReturnValue AutoCentralMonitor::OnSpeedChange()
{
  _theSpeed.UpdateSpeed(currentSpeed);
  If (currentSpeed 
>= 120)
  {
    _theAlarm.Alert();
  }
}

 

这个代码有些问题:

首先,扩展性很差,如果某天我们想换一个品牌的时速表或报警器,但是新的时速表和报警器提供的是完全不同的类名和接口,比如时速表提供类名BenzSpeed和接口Display,警报器提供类名Alarm和接口StartNoise,那么类AutoCentralMonitor的内部实现就要跟着变,那是多么枯燥和累人的工作,要命的是很可能产生新的Bug。2010年的丰田汽车采用的是美国一家公司的油门踏板(当然丰田被美国人搞的很惨),现在油门踏板出了大问题,也许将来他们得考虑使用其他厂商的踏板,如果更换油门踏板导致丰田得重新设计汽车的内部结构,那它要多惨就有多惨;

其次,作为一个独立的个体,类AutoCentralMonitor需要了解另外两个类Speed和Alarm的接口,需要知道如何使这两个类的对象工作,如果类Speed和Alarm里提供了一系列的接口,需要组合调用才能工作,那么AutoCentralMonitor的工作就变得更复杂;

第三,如果我们要给车载电子系统增加其他的设备,比如发动机转速表,那么AutoCentralMonitor又得增加相应的属性和方法。

 

总之,图1的设计使AutoCentralMonitor的工作变得很杂乱,如果有大量的设备需要管理,它就要了解每个设备的开放接口并且要知道怎么调用才能使其工作,那么对于该类的维护简直就是一个噩梦。面向对象有一条原则是“依赖接口、而非依赖于实现”,用该原则实现各个设备的代码能得到一些设计良好的类,当前的依赖关系是顶层的类依赖于底层的类,这符合正常思维、也是结构化编程的典型依赖关系,但是我们的目标是尝试着将AutoCentralMonitor的跟其他的类独立开来,以使类AutoCentralMonitor的变化独立于外部设备的变化,实现这种设计的方法就是“依赖倒置原则”,使Speed、Alarm等设备类成为AutoCentralMonitor类的客户(Client),即使底层的实现依赖高层的实现。如果AutoCentralMonitor在事件发生时只需要给Client一个通知,并发送一些内部数据,然后由Client在接受到的数据中选择自己感兴趣的部分并做进一步的处理那么AutoCentralMonitor就不用再关心各个Client的内部结构和实现细节。至此就能想到每个Client应该提供一个类似OnEvent/OnEventArrive/OnNotification/OnNotify之类的接口,而AutoCentralMonitor应该包含一个类似Notify()的方法将事件通知各个Client。

 

注:“依赖倒置原则”— The Dependency Inversion Principle, 简称DIP,出自Robert C Martin的<敏捷开发>一书,这本书以敏捷开发为主题似乎是在教授一种新的软件开发的方法论,其实里面夹杂了很多面向对象、设计模式的思想,感兴趣的读者可以看看。本人认为,即使不看敏捷开发、不看设计模式,我们在编程中也会不经意中已经用过其中的部分,只是我们不知道这些内容已经可以作为一种理论或方法来使用,这些知识和经验在我们的脑海里是零散、杂乱的,但是当系统的看过相关方面的书籍后,就会对相关的知识形成系统的认识,在实际开发中就可能会考虑是否可以加以运用,另外,在研读现成的代码的时候可能理解的更快、认识的更深。

 

继续上面的话题,我们抽象出一个只提供Update()接口的纯虚类IObserver,观察者类Alarm和Speed分别继承并实现该类的Update接口,而主题类AutoCentralMonitor持有一个IObserver类型的容器存储所有向它提交注册的观察者(Client),这样所有的观察者对于AutoCentralMonitor来说都是IObserver类型,从而简化了主题类处理方法,先看一下改进后的UML图:

 

图 2

 

这个设计的实现如下 

#include <iostream>
#include 
<list>

using namespace std;

class IObserver
{
public:
  
virtual void Update(int= 0;

public:
  
virtual ~IObserver(){};
};

class AutoCentralMonitor
{
public:
  AutoCentralMonitor() : _clientVec(
0), _speed(0)
  {
  }

  
void Accelerate()
  {
    ResetSpeed();
    
for (int i=0; i<122++i) {
      
++_speed;
      Notify();
    }
  }
  
  
void Register(IObserver& client)
  {
    _clientVec.push_back(
&client);
  }
  
void UnRegister(IObserver& client)
  {
    InsideIterator it 
= _clientVec.begin();
    
for (; it != _clientVec.end(); it++) {
      
if ((*it) == &client) {
        _clientVec.erase(it);
        /**
        * must reset it since we just
        * remove one elment from list
        */
        it 
= _clientVec.begin();         
      }
    }
  }

  
~AutoCentralMonitor()
  {
    _clientVec.clear();
  }

private:
  
int ResetSpeed()
  {
    
return _speed = 0;
  }
  
void Notify()
  {
    InsideIterator it 
= _clientVec.begin();
    
while (_clientVec.end() != it) {
      (
*it++)->Update(_speed);
    }
  }

private:
  typedef list
<IObserver*>::iterator InsideIterator;
  
int _speed;
  /**
  * Use std::list as container to manage observers
  */
  list
<IObserver*> _clientVec;
};


class Alarm : public IObserver
{
public:
  
virtual void Update(int speed)
  {
    
if (speed >= 120) {
      cout 
<< "Be careful, you drive too fast, current speed is "
           
<< speed << " KM/H" << endl;
    }
  }
};

class Speed : public IObserver
{
public:
  
virtual void Update(int speed)
  {
    
if (speed % 20 == 0) {
      cout 
<< speed << endl;
    }
  }
};


int _tmain(int argc, _TCHAR* argv[])
{
  AutoCentralMonitor  monitor; 
  Speed               speed;
  Alarm               alarm;

  monitor.Register(speed);
  monitor.Register(alarm);
  monitor.Accelerate();

  cout 
<< "Now disable the Alarm\n";
  monitor.UnRegister(alarm);
  monitor.Accelerate();

  
return 0;
}

 

上面的程序比较简单,不再做过多说明,可能有的初学C++的人会对程序中的List、iterator之类的字眼不大明白,那么请找一本经典的C++教材看看很容易就明白了,推荐<C++ Primer>。

 

顺便提一下,国内的C++教材通常比较烂、误人子弟,本人就深受其害。在本科期间我曾翻过数个出版社的多个教材、学习也算认真,但是竟然没有一个提及STL的,包括清华大学出版的那本<C++程序设计教程>(本科期间我对清华大学出版社出版的书一直都还比较信赖),以至于那时我根本不知道C++还有个STL,真是惭愧。刚读研时看到<C++ Primer>里有那么多的内容国内的教程里竟然只字不提,自此技术类的书籍我就只读国外的著作,也因此收获良多。另外,我认为想学好C++,三本书必读,分别是<C++ Primer>、<Effective C++>、<More Effective C++>,这三本书吃透、才敢说精通,也许还应该推荐<Thinking in C++>和<C++设计新思维>,但是我没有系统的看过的就不敢盲目推荐。 

 

其运行结果

20
40
60
80
100
120
Be careful, you drive too fast, current speed 
is 120 KM/H
Be careful, you drive too fast, current speed 
is 121 KM/H
Be careful, you drive too fast, current speed 
is 122 KM/H
Now disable the Alarm
20
40
60
80
100
120

 

从运行结果可知,当Alarm和Speed将自己注册为Observer之后,二者便得到了AutoCentralMonitor更新的信息,然后Alarm的警报被解除了(UnRegister),之后当超速的时候便不再鸣笛警报。

 

至此,如果要换个报警器,或者有一个新的设备比如自动油门控制系统对车速感兴趣,当车速达到120KM/H时就不在自动控制不再加速,那么该类只需要从IObserver类派生出来并重新实现Update接口,然后在使用的时候(这里是main)向AutoCentralMonitor注册一下就可以了。看,多么漂亮,程序员的生活也可以变得这么精彩!!!

 

有了这个设计,我想回头看看Observer的标准定义,毕竟我已经很久没有看设计模式了,我的设计可能会存在一些问题。果然,标准的Observer模式设计图是这样的

图 3
   

本文无意完全遵照四人帮的做法实现自己的代码,比如我不打算实现什么GetState、SetState之类的方法,但是有一个点必须注意,那就是Observer的标准定义中有一个Subject接口,这点必须加以考虑。增加Subject接口使我们可以独立的附庸主题或观察者,因为二者并非紧耦合,这样改变主题或观察者中一方并不会影响另一方,只要他们之间的接口被遵守,我们就可以自由的改变他们。这种改进最明显的好处就在于,每当一个类想成为主题的时候,它只需要继承Subject类就行了,简化了类的维护,而对于观察者Observer来说,由于所有的主题ConcreteSubject都遵守接口Subject的规定,所以可以被统一的当作Subject类型看待,避免了由于每个主题ConcreteSubject提供不同的添加/删除函数而引起观察者Observer的变化(比如SubjectA提供Register/UnRegister方法而SubjectB提供Attach/DeAttach方法)。

对于本例,由于只有一个主题AutoCentralMonitor,前面的代码已经可以说明问题,不过为了应对将来可能的变化以及符合"面向接口而非面向实现"的原则,现在对其做如下改进,其中红色部分是新增加的类,绿色部分是改进现有的类,其他保持不变

class ISubject
{
public:
  ISubject(): _clientVec(0)
  {
  }

  
void Register(IObserver& obj)
  {
    _clientVec.push_back(&obj);
  }

  
void UnRegister(IObserver& obj)
  {
    InsideIterator it = _clientVec.begin();
    
for (; it != _clientVec.end(); ++it) {
      
if ((*it) == &obj) {
        _clientVec.erase(it);
        it = _clientVec.begin();
      }
    }
  }

  
virtual ~ISubject()
  {
    _clientVec.clear();
  }

protected:
  
virtual void Notify() = 0;

protected:
  typedef list<IObserver*>::iterator InsideIterator;
  list<IObserver*> _clientVec;
};

class AutoCentralMonitor : public ISubject
{
public:
  AutoCentralMonitor() : _speed(0)
  {
  }

  
void Accelerate()
  {
    ResetSpeed();
    
for (int i=0; i<122++i) {
      
++_speed;
      Notify();
    }
  }

  
~AutoCentralMonitor(){}

protected:
  
virtual void Notify()
  {
    InsideIterator it = _clientVec.begin();
    
while (_clientVec.end() != it) {
      (*it++)->Update(_speed);
    }
  }

private:
  
int ResetSpeed()
  {
    
return _speed = 0;
  }

private:
  
int _speed;
};

 

运行结果没有变化,不再附图。至此已经完成了简单的Observer模式的设计与实现,截至目前为止,主题AutoCentralMonitor的数据speed都是通过IObserver接口的Update参数传递给观察者的,这种方式就是推模式,也即Push,这种方式有个缺点,就是不管观察者想不想得到主题的数据或者不管该数据是不是他想要的,它都必须接收,至于怎么处理那就是观察者自己的事了,这无疑会造成资源浪费。所以下面将要描述Observer模式的另一种形式拉模式,也即Pull。不过在进行Pull方式之前,对于Push方式我还有点疑问想再研究研究,如前所述,Push方式是通过IObserver的Update接口传送数据的,在我们的例子中该方法的定义为 

void Update(int);

 

那么如果要传送更多的数据呢,可能会是这样 

void Update(intintfloatstring);

 

这种方式没错,只是如果传大量的数据这个函数的参数列表会很长,看着很难看,维护起来困难,程序员的疏忽容易传递错误的参数,生成参数的副本也会增加调用开销,反正是不好,这种情况下可以将要传递的参数定义为一个结构体,然后Update方法里传递一个结构体类型的参数,像这样

typedef struct{
  
int     data1;
  
int     data2;
  
float   data3;
  
string  data4;
}Data_t;
 
void Update(Data_t data);

 

这样看起来简洁了很多,不过有一个值得关注的问题,函数调用的时候会生成实参的拷贝给调用函数,是当Data_t很大的时候这种拷贝的代价会比较大,为了减小这种开销,可以将Update的参数改为Data_t类型的指针,现在的Update仍然不是很完美,因为它只能接受Data_t类型的指针,为了使Update方法能够接收任意类型的参数,我们的思维又会回到"面向接口而非面向实现"上面去,但是C++不像C#,它没有供所有对象共有的基类型,解决方案就是万能的void*,将Update的参数声明为void类型的指针,然后主题Subject直观调用Update,至于具体当作什么类型使用则由观察者Observer自己决定,改进后的Observer接口类如下 

class IObserver
{
public:
  
virtual void Update(void* pData= 0;

public:
  
virtual ~IObserver(){};
};

 

对应的观察者类需自行转换指针,以Alarm为例 

class Alarm : public IObserver
{
public:
  
virtual void Update(void* pData)
  {
    
int* speed = static_cast<int*>(pData);
    
// the other opreation
  }
};

 

现在该看看Pull方法在观察者模式中的运用了,如前所述,Pull方式就是主题类调用Update接口通知观察者类的时候不显示的发送数据给各个观察者,而是由观察者自己去索取它们感兴趣的数据,可想而知,主题类必须得提供一些公开的方法供观察者类调用以获取数据,这样的坏处一是破坏了主题类的隐私、也即它的封装性;第二个缺点是当观察者想获取主题类的多个数据的时候它得多次调用主题类的方法才能完成,过程比较复杂冗长。

 

从实现的角度去看Pull方法的话,由于IObserver的Update接口是供所有主题使用,要统一的对待不同类型的主题实现类,Update应该以主题类的基类指针为参数,如下所示

class IObserver
{
public:
  
virtual void Update(ISubject* pSubject= 0;

public:
  
virtual ~IObserver(){};
};

  

同时观察者类只实现这一个接口,每个观察者类都应该知道它想要的是哪个主题的数据,也就是它知道该主题的具体类型,因此观察者实现类在实现Update的时候应该将受到的主题基类类动态转换为自己感兴趣的主题实现类,因为如果主题实现类不是观察者实现类感兴趣的类型,那么动态类型转换会失败,返回空指针,因此用在这里非常合适。其大概实现是这样

class ConcreteObserver : public IObserver
{
public:
  
virtual void Update(ISubject* pSubject)
  {
    
AutoCentralMonitor* pAcm = dynamic_cast<AutoCentralMonitor*>(pSubject);
    
if (pAcm)
    {
       
// some operation
    }
  }
};

 

Pull方式的实现全代码

  1 #include <iostream>
  2 #include <list>
  3 
  4 using namespace std;
  5 
  6 /**
  7 * Foward declartion
  8 */
  9 class ISubject;
 10 
 11 class IObserver
 12 {
 13 public:
 14   virtual void Update(ISubject* pSubject) = 0;
 15 
 16 public:
 17   virtual ~IObserver(){};
 18 };
 19 
 20 class ISubject
 21 {
 22 public:
 23   ISubject(): _clientVec(0)
 24   {
 25   }
 26 
 27   void Register(IObserver& obj)
 28   {
 29     _clientVec.push_back(&obj);
 30   }
 31 
 32   void UnRegister(IObserver& obj)
 33   {
 34     InsideIterator it = _clientVec.begin();
 35     for (; it != _clientVec.end(); ++it) {
 36       if ((*it) == &obj) {
 37         _clientVec.erase(it);
 38         it = _clientVec.begin();
 39       }
 40     }
 41   }
 42 
 43   virtual ~ISubject()
 44   {
 45     _clientVec.clear();
 46   }
 47 
 48 protected:
 49   virtual void Notify(ISubject* pSubject)
 50   {
 51     InsideIterator it = _clientVec.begin();
 52     while (_clientVec.end() != it) {
 53       (*it++)->Update(pSubject);
 54     }
 55   }
 56 
 57 protected:
 58   typedef list<IObserver*>::iterator InsideIterator;
 59   list<IObserver*> _clientVec;
 60 };
 61 
 62 class AutoCentralMonitor : public ISubject
 63 {
 64 public:
 65   AutoCentralMonitor() : _speed(0)
 66   {
 67   }
 68 
 69   void Accelerate()
 70   {
 71     ResetSpeed();
 72     for (int i=0; i<122++i) {
 73       ++_speed;
 74       /**
 75       * same with ISubject::Notify(this), pass itself as
 76       * a ISubject object param to be used by observers.
 77       */
 78       Notify(this);
 79     }
 80   }
 81 
 82   int GetSpeed() const
 83   {
 84     return _speed;
 85   }
 86 
 87   ~AutoCentralMonitor(){}
 88 
 89 private:
 90   int ResetSpeed()
 91   {
 92     return _speed = 0;
 93   }
 94 
 95 private:
 96   int _speed;
 97 };
 98 
 99 
100 class Alarm : public IObserver
101 {
102 public:
103   virtual void Update(ISubject* pSubject)
104   {
105     AutoCentralMonitor* pAcm = dynamic_cast<AutoCentralMonitor*>(pSubject);
106     if (pAcm)
107     {
108       int speed = pAcm->GetSpeed();
109       if (speed >= 120) {
110         cout << "Warning... Current Speed is " << speed << endl;
111       }
112     }
113   }
114 };
115 
116 class Speed : public IObserver
117 {
118 public:
119   virtual void Update(ISubject* pSubject)
120   {
121     AutoCentralMonitor* pAcm = dynamic_cast<AutoCentralMonitor*>(pSubject);
122     if (pAcm)
123     {
124       int speed = pAcm->GetSpeed();
125       if (speed % 20 == 0) {
126         cout << speed << endl;
127       }
128     }
129   }
130 };
131 
132 
133 int main(int argc, char* argv[])
134 {
135   AutoCentralMonitor  monitor;
136   Speed               speed;
137   Alarm               alarm;
138 
139   monitor.Register(speed);
140   monitor.Register(alarm);
141   monitor.Accelerate();
142 
143   cout << "Now disable the Alarm\n";
144   monitor.UnRegister(alarm);
145   monitor.Accelerate();
146 
147   return 0;
148 }

  

需要注意到是上面代码中的第78行 Notify(this); 这一句代码,这里是主题通知Observer的地方,它将主题对象自身的指针(this)作为参数传递给Update,这是确保第105行和第121行的观察者类的动态向下转换能够成功的保证。

 

运行结果

20
40
60
80
100
120
Warning... Current Speed 
is 120
Warning... Current Speed 
is 121
Warning... Current Speed 
is 122
Now disable the Alarm
20
40
60
80
100
120

 

至此,我们完成了Observer模式的两种方式Push和Pull的实现,实际上,Push和Pull这两种方法通常是混合使用的,做法就是Update里既提供ISubject类型的主题的对象指针,也提桶void*的数据指针,因此Update接口看起来像这样

virtual void Update(ISubject* pSubject, void* pData) = 0;

 

其使用方法前文已分别展示过,不再赘述,另外,ConcreteSubject类里有时候需要设置一些状态函数,以便更好的控制调用Update的时机,实现比较简单,也不再过多描述,关于Observer模式到此结束,谢谢阅读。

 

posted on 2010-05-09 22:26  Simba Yang  阅读(759)  评论(0编辑  收藏  举报