首页 > 代码库 > 观察者模式
观察者模式
题记:观察者模式在很多源码中都有应用,最近看的gloox库采用的也是观察者模式还有moduo网络库,在此特意记录一下,共同加油!
1、意图
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
2、动机
将一个系统分割成一系列相互协作的类有一种常见的副作用:需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,因为这样降低了它们的可重用性。
3、适用性
在以下任一情况下可以使用观察者模式:
1)当一个抽象模型有两个方面,其中一个方面依赖于另一方面。将这两者封装在独立的对象中以使它们可以各自独立地改变和复用。
2)当对一个对象的改变需要同时改变其它对象,而不知道具体有多少对象有待改变。
3)当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之,你不希望这些对象时紧密耦合的。
4、参与者
1)Subject(目标)
----目标知道它的观察者。可以有任意多个观察者观察同一个目标。
-----提供注册和删除观察者对象的接口。
2)Observer(观察者)
----为那些在目标发生改变时需获得通知的对象定义一个更新接口。
3)ConcreteSubject(具体目标)
----将有关状态存入各ConcreteObserver对象。
-----当它的状态发生改变时,向它的各个观察者发出通知。
4)ConcreteObserver(具体观察者)
-----维护一个指向ConcreteSubject对象的引用。
------存储有关状态,这些状态应与目标的状态保持一致。
-------实现Observer的更新接口以使自身状态与目标的状态保持一致。
5、协作
1)当ConcreteSubject发生任何可能导致其观察者与其本身状态不一致的改变时,它将通知它的各个观察者。
2)在得到一个具体目标的改变通知后,ConcreteObserver对象可向目标对象查询信息。ConcreteObserver使用这些信息以使它的状态与目标对象的状态一致。
注意:发出改变请求的Observer对象并不立即更新,而使将其推迟到它从目标得到一个通知之后。Notify不总是由目标对象调用。它也可被一个观察者或其他对象调用。
6、效果
观察者模式允许你独立的改变目标和观察者。你可以单独复用目标对象而无需同时复用到其观察者,反之亦然。它也使你可以在不改动目标和其他的观察者的前提下增加观察者。
下面是观察者模式的其它一些优缺点:
1)目标和观察者间的抽象耦合
一个目标所指的的仅仅是它有一系列观察者。每个都符合抽象的Observer类的简单接口。目标不知道任何一个观察者属于哪一个具体的类。这样目标和观察者之间的耦合式抽象的和最小的。
因为目标和观察者不是紧密耦合的,它们可以属于一个系统中的不同抽象层次。一个处于较低层次的目标对象可与一个处于较高层次的观察者通信并通知它,这样就保持了系统层次的完整。如果目标和观察者混在一块,那么得到的对象要么横贯两个层次(违反了层次性),要么必须放在这两层的某一层中。
2)支持广播通信
不像通常的请求,目标发送的通知不需指定它的接收者。通知被自动广播给所有已向该目标对象登记的有关对象。目标对象并不关心到底有多少对象对自己感兴趣;它唯一的责任就是通知它的各观察者。这给了你在任何时刻增加和删除观察者的自由。处理还是忽略一个通知取决于观察者。
3)意外的更新
因为一个观察者并不知道其他观察者的存在,它可能对改变目标的最终代价一无所知。在目标上一个看似无害的操作可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新。此外,如果依赖准则的定义或维护不当,常常会引起错误的更新,这种错误通常很难捕捉。
简单的更新协议不提供具体细节说明目标中什么被改变了,这就使得上述问题更加严重,如果没有其他协议帮助观察者发现什么发生了改变,它们可能会被迫尽力减少改变。
7、实现
1)创建目标到其观察者之间的映射
一个目标对象跟踪它应通知的观察者的最简单的方法是显示地在目标中保存对它们的引用。然而,当目标很多而观察者很少时,这样存储可能代价太高。一个解决办法是用时间换空间,用一个关联查找机制(例如一个Hash表)来维护目标到观察者的映射。这样一个没有观察者的目标就不产生存储开销。但另一方面,这一方法增加了访问观察者的开销。
2)观察多个目标
在某些情况下,一个观察者依赖于多个目标可能是有意义的。例如,一个表格对象可能依赖于多个数据源。在这种情况下,必须扩展Update接口以使观察者知道是哪一个目标送来的通知。目标对象可以简单地将自己作为Update操作的一个参数,让观察者知道应去检查哪一个目标。
3)谁触发更新
目标和它的观察者依赖于通知机制来保持一致。但到底哪一个对象调用Notify来触发更新?此时有两个选择:
a)由目标对象的状态设定操作在改变目标对象的状态后自动调用Notify。这种方法的优点是客户不需要记住要在目标对象上调用Notify,缺点是多个连续的操作会产生多次连续的更新,可能效率较低。
b)让客户负责在适当的时候调用Notify。这样做的优点是客户可以在一系列的状态改变完成后再一次性的触发更新,避免了不必要的中间更新。缺点是给客户增加了触发更新的责任。由于客户可能会忘记调用Notify,这种方式较易出错。
4)对已删除目标的悬挂引用
删除一个目标时应注意不要在其观察者中遗留对该目标的悬挂引用。一致避免悬挂引用的方法是,当一个目标被删除时,让它通知它的观察者将对该目标的引用复位。一把来说,不能简单的删除观察者,因为其他的对象可能会引用它们,或者也可能它们还在观察其他的目标。
5)在发出通知前确保目标的状态自身是一致的
在发出通知前确保状态自身一致这一点很重要,因为观察者在更新其状态的过程中需要查询目标的当前状态。
当Subject的子类调用继承的该项操作时,很容易无意中违反这条自身一致的准则。例如,下面的代码系列中,在目标尚处于一种不一致的状态时,通知就被触发了:
void MySbuject::operation(int newValue)
{
BaseClassSubject::Operation(newValue);
//trigger notification
_myInstVar +=newValue;
//update subclass state(too late!)
}
你可以用抽象的Subject类中的末班方法发送通知来避免这种错误。定义那些子类可以重定义的原语操作,并将Notify作为模板方法中的最后一个操作,这样当子类重定义了Subject的操作时,还可以保证该对象的状态是自身一致的。
void Text::Cut(TextRange r)
{
ReplaceRange(r);
Notify();
}
顺便提一句,在文档中记录是哪一个Subject操作触发通知总是应该的。
6)避免特定于观察者的更新协议----推/拉模型
观察者模式的实现经常需要让目标广播关于其改变的其它一些信息。目标将这些信息作为Update操作的一个参数传递出去。这些信息的量可能很小也可能很大。
一个极端情况是,目标向观察者发送关于改变的详细信息,而不管他们需要与否。我们称之为推模型。另一个极端是拉模型,目标除最小通知外什么也不送出,而在此之后由观察者显式地向目标询问细节。
拉目标强调的是目标不知道它的观察者,而退模型假定目标制定一些观察者的需要的信息。推模型可能使得观察者相对难以复用,因为目标对观察者的假定可能并不总是正确的。另一方面,拉模型可能效率较差,因为观察者对象需在没有目标对象帮助的情况下确定什么改变了。
7)显示地指定感兴趣的改变
你可以扩展目标的注册接口,让各观察者注册为仅对特定事件感兴趣,以提高更新的额效率。当一个事件发生时,目标仅通知那些已注册为对该事件感兴趣的观察者。支持这种做法的一种途径是,对使用目标对象的方面的概念。可用如下代码将观察者对象注册为对目标对象的某特定事件感兴趣:
voidSubject::Attach(Observer*,Aspect& interest);
此处interest指定感兴趣的事件。在通知的时刻,目标将这方面的改变作为Update操作的一个参数提供给它的观察者,例如:
void Observer::Update(Subject*,Aspect& interest);
8)封装复杂的更新语义
当目标和观察者间的依赖关系特别复杂时,可能需要一个维护这些关系的对象。我们称这样的对象为更改管理器。它的目的是尽量减少观察者反映其目标的状态变化所需的工作量。例如,如果一个操作设计到对几个相互依赖的目标进行改动,就必须保证仅在所有的目标都已更改完毕后,才一次性地通知它们的观察者,而不是每个目标都通知观察者。
ChangeManager有三个责任:
a)它将一个目标映射到它的观察者并提供一个接口来维护这个映射。这就不需要由目标来维护对其观察者的引用,反之亦然。
b)它定义一个特定的更新策略。
c)根据一个目标的请求,它更新所有依赖于这个目标的观察者。
代码实例:
一个抽象类定义了Observer接口:
class Subject;
class Observer
{
public:
virtual ~Observer();
virtual void Update(Subject* theChangeSubject) = 0;
protected:
Observer();
}
这种实现方式支持一个观察者有多个目标。当观察者观察多个目标时,作为参数传递给Update操作的目标让观察者可以判定是哪一个目标发生了改变。
类似地,一个抽象类定义Subject接口:
class Subject
{
public:
virtual ~Subject();
virtual void Attach(Observer* );
virtual void Detach(Observer* );
virtual void Notify();
protected:
Subject();
private:
List<Observer*> *_observers;
};
void Subject::Attach(Observer* o)
{
_observer->Append(o);
}
void Subject::Detach(Observer* o)
{
_observers->Remove(o);
}
void Subject::Notify()
{
ListIterator<Observer*> i(_observers);
for(i.First();!i.IsDone();i.Next())
{
i.CurrentItem()->Update(this);
}
}
ClockTimer是一个用于存储和维护一天时间的具体目标。它每秒钟通知一次它的观察者,ClockTimer提供了一个接口用于取出单个的时间单位如小时,分钟和秒。
class ClockTimer:public Subject
{
public:
ClockTimer();
virtual int GetHour();
virtual int GetMinute();
virtaul int GetSecond();
void Tick();
};
Tick操作由一个内部计时器以固定的时间间隔调用,从而提供一个精确地时间基准。Tick更新ClockTimer的内部状态并调用Notify通知观察者:
class ClockTimer:public Subject
{
public:
ClockTimer();
virtual int GetHour();
virtual int GetMinute();
virtaul int GetSecond();
void Tick();
};
Tick操作由一个内部计时器以固定的时间间隔调用,从而提供一个精确地时间基准。Tick更新CLockTimer的内部状态并调用Notify通知观察者:
void ClockTimer::Tick()
{
//Update internal time-keeping state
//...
Notify();
}
现在我们可以定义一个DIgitalClock类来显示时间,它从一个用户界面工具箱提供的Widget类继承了它的图形功能。通过继承Observer,Observer接口被融入DigitalClock的接口:
class DigitalClock:public Widget,public Observer
{
public:
DigitalClock(CLoclTimer* );
virtual ~DigitalClock();
virtual void Update(Subject* );
//overrides Observer opratation
virtual void Draw();
private:
CLockTimer* _subject;
}
DigitalClock::DigitalClock(ClockTimer* s)
{
_subject = s;
_subject->Attack(this);
}
DigitalClock::~DigitalClock()
{
_subject->Detach(this);
}
在Update操作画出时钟图形之前,它进行检查,以保证发出通知的目标是该时钟的目标:
void DigitalClock::Update(Subject* theChangeSubject)
{
if(theChangeSubject == _subject)
{
Draw();
}
}
vodi DigitalClock::Draw()
{
//get the new values from the subject
int hour = _subject->GetHour();
int minute =_subject->GetMinute();
//etc
//draw the digital clock
}
一个AnalogClock和一个DigitalClock,它们总是显示相同时间:
ClockTimer* timer = new ClockTImer;
AnalogClock* analgoClock = new AnalogClock(timer);
DigitalClock* digitalClock = new DigitalClock(timer);
一旦timer走动,两个时钟都会被更新并正确地重新显示。