1.意图
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
2.别名
依赖(Dependents),发布-订阅(Publish-Subscribe)
3.动机
将一个系统分割成一系列相互协作的类有一个常见的副作用:需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,因为这样降低了它们的可重用性。
许多图形用户界面工具箱将用户应用的界面表示与底下的应用数据分离。定义应用数据的类和负责界面表示的类可以各自独立的复用。当然,它们也可以一起工作。一个表格对象和一个柱状图对象可使用不同的表示形式描述同一个应用数据对象的信息。表格对象和柱状对象互相并不知道对方的存在,这样可以根据需要单独复用表格或柱状图。但在这里时它们表现的似乎互相知道。当用户改变表格中的信息时,柱状图能立即反映这一变化,返过来也是如此。
这一行为意味着表格对象和棒状图对象都依赖于数据对象,因此数据对象的任何状态改变都应立即通知它们。同时也没有理由将依赖于该数据对象的数目限定为两个,对相同的数据可以有任意数目的不同用户界面。
Observer模式描述了如何建立这种关系。这一模式中的关键对象是目标(subject)和观察者(observer)。一个目标可以有任意数目的依赖它的观察者。一旦目标的状态发生改变,所有的观察者都得到通知。作为对这个通知的响应,每个观察者都将查询目标以使其状态与
目标的状态保持同步。
这种交互也称为发布-订阅(publish-subscribe)。目标是通知的发布者。它发出通知时并不需要知道谁是它的观察者。可以有任意数目的观察者订阅并接收通知。
4.适用性
在以下任一情况下可以使用观察者模式:
- 当一个抽象模型有两个方面,其一个方面依赖于另一方面。将这二者封装在独立的对象中,以使它们可以各自独立的改变和复用;
- 当对一个对象改变需要同时改变其他对象,而又不知道具体有多少对象有待改变;
- 当一个对象必须通知其他对象,而它又不能假定其它对象是谁。换言之,这些对象是松耦合的,甚至相互之间不知道对方的存在。
- 结构
6. 参与者
- Subject(目标)
- 目标知道它的观察者。可以有任意多个观察者观察同一个目标
- 提供注册和删除观察者对象的接口。
- Observer(观察者)
1)为在目标发生改变时需要获得通知的观察者对象定义一个更新接口。
- ConcreteSubject(具体目标)
- 将有关状态存入各ConcreteObserver对象;
- 当它的状态发生改变时,向它的各个观察者发出通知。
- ConreteObserver(具体观察者)
- 维护一个指向ConcreteSubject对象的引用;
- 存储有关状态,这些状态应与目标的状态保持一致;
- 实现Observer的更新接口以使自身状态与目标的状态保持一致。
- 协作
- 当ConcreteSubject发生任何可能导致其观察者与其本身状态不一致的改变时,它将通知它的各个观察者。
- 在得到一个具体目标的改变通知后,ConcreteObserver对象可向目标对象查询信息。
ConcreteObserver使用这些信息以使它的状态与目标对象的状态一致。
下面的交互图说明了一个目标对象和两个观察者之间的协作:
8. 效果
Observer模式允许独立的改变目标和观察者。你可以单独复用目标对象而无需同时复用其观察者,反之亦然。它也使你可以在不改动目标和其他的观察者的前提下增加观察者。
下面是观察者模式的一些优缺点:
- 目标和观察者间的抽象耦合。 一个目标所知道的仅仅是它有一系列观察者,每个都符号抽象的Observer类的简单接口。目标不知道任何一个观察者属于哪一个具体的类。这样目标和观察者之间的耦合是抽象的和最小的。
- 支持广播通信
不像通常的请求,目标发送的通知不需要指定它的接收者。通知被自动广播给所有已经向该目标对象登记的有关观察者对象。目标对象不关心到底有多少对象对自己感兴趣;它唯一的责任就是通知它的各观察者。这给了目标在任何时候增加和删除观察者的自由。处理还是忽略一个通知取决于观察者。
- 意外的更新 因为一个观察者并不知道其它观察者的存在,它可能对改变目标的最终代价一无所知。在目标上一个看似无害的动作可能会引起一系列对观察者以及依赖这些观察者的那些对象的更新。此外,如果依赖准则的定义或维护不当,常常会引起错误的更新,这种错误通常很难捕捉。
- 实现
这一节讨论一些与实现依赖机制有关的问题。
- 创建目标到其观察者之间的映射
一个目标对象跟踪它应通知的观察者的最简单的方法是显式地在目标保存对它们地引用。然而,当目标很多而观察者较少时,这样存储可能代价很高。一个解决方法是用一个关联查找机制(例如,一个hash表)来维护目标到观察者地映射。这样一个没有观察者的目标就不产生存储开销。但另一方面,这一方法增加了访问观察者的开销。
- 观察多个目标
在某些情况下,一个观察者依赖于多个目标可能是有意义的。例如,一个表格对象可能依赖于多个数据源。在这种情况下,必须扩展Update接口以使观察者知道是哪一个目标送来的通知。目标对象可以简单地将自己作为Update操作的一个对象参数,让观察者知道应去检查哪个目标。
- 谁触发更新?
有两个选择:
a) 由目标对象的状态设定操作在改变目标对象的状态后自动调用Notify。这种方法的优点是客户不需要记住要在目标对象上调用Notify,缺点是多个连续的操作会产生多次连续的更新,可能效率较低。
b) 让客户负责在适当的时候调用Notify。这样做的优点是客户可以在一系列的状态改变完成后再一次性地触发更新,避免了不必要地中间更新。缺点是给客户增加了触发更新地责任。由于客户可能会忘记调用Notify,这种方式容易出错。
- 对已删除目标的悬挂引用
删除一个目标时应注意不要在其观察者中遗留对该目标的悬挂引用。一种避免悬挂引用的方法是,当一个目标被删除时,让它通知它的观察者将对该目标的引用复位。一般来说,不能简单地删除观察者,因为其他的对象可能会引用它们,或者也可能它们还在观察其他其他的目标。
- 在发出通知前确保目标的状态自身是一致的
在发出通知前确保状态自身一致这一点很重要,因为观察者在更新其状态过程中需要查询目标的当前状态。
可以用抽象的Subject类中的模板方法发送通知来避免这种错误。定义那些子类可以重定义的原语操作,并将Notify作为模板方法中的最后一个操作,这样当子类重定义了Subject的操作时,还可以保证该对象的状态是自身一致的。
Void Text::Cut(TextRange r){
ReplaceRange(r);
Notify();
}
在文档中记录是哪一个Subject操作触发通知总是应该的。
- 避免特定于观察者的更新协议—推拉模型
观察者模式的实现经常需要让目标广播关于其改变的其他一些信息。目标将这些信息作为Update操作一个参数传递出去。这些信息的量可能很小,也可能很大。
一个极端的情况是,目标向观察者发送关于改变的详细信息,而不管它们需要与否。我们称之为推模型(push model)。另一个极端是拉模型(pull model);目标除最小通知外,什么也不送出,而在此之后由观察者显式地向目标询问细节。
拉模型强调的是目标不知道它的观察者,而推模型假定目标知道一些观察者的需要的信息。推模型可能使得观察者相对复用,因为对观察者的假定可能并不总是正确的。另一方面,拉模型可能效率较低,因为观察者对象在没有目标对象帮助下确定什么改变了。
- 显示地指定感兴趣地改变
可以扩展目标的注册接口,让各观察者注册为仅对特定事件感兴趣,以提高更新的效率。当一个事件发生时,目标仅通知那些已经注册为对该事件感兴趣的观察者。支持这一做法的一种途径是,使用目标的方面(aspects)的概念。可用如下代码将观察者对象注册为对目标对象的特定事件感兴趣:
Void Subject::Attach(Obsever*, Aspect & interest);
此处interset指定感兴趣的事件。在通知的时刻,目标将这方面的改变作为Update操作的一个参数提供给它的观察者,例如:
Void Observer::Update(Subject*, Aspect& interest);
- 封装更复杂的更新语义
当目标和观察者间的依赖关系特别复杂时,可能需要一个维护这些关系的对象。我们称这样的对象为更改管理器(ChangeManager)。它的目的是尽量减少观察者反映其目标的状态变化所需的工作量。例如,如果一个操作涉及到对几个相互依赖的目标进行改动,就必须保证仅在所有目标都更改完毕后,才一次性地通知它们的观察者,而不是每个目标都通知观察者。
ChangeManager有三个责任:
a)它将一个目标映射到它的观察者并提供一个接口来维护这个映射。这不需要由目标来维护对其观察者的引用,反之亦然。
b)它定义一个特定的更新策略;
c) 根据一个目标的请求,它更新所有依赖于这个目标的观察者。
上面的框图描述了一个简单的基于ChangeManager的Observer模式的实现。有两种特殊的ChangeManager。SimpleChangeManager总是更新每一个目标的所有观察者,比较简单。相反,DAGChangeManager处理目标及其观察者之间依赖关系构成的无环有向图。当一个观察者观察多个目标时,DAGChangeManager要比SimpleChangeManager更好一些。在这种情况下,两个或多个目标中产生的改变可能会产生冗余的更新。
ChangeManager是一个中介模式实例,通常只有一个全局的单例。
- 结合目标类和观察类
用不支持多重继承的语言书写的类库通常不单独定义Subject和Observer类。而是将它们的接口结合到一个类中。这就允许定义一个既是一个目标又是一个观察者的对象,而不需要多重继承。例如,在SmallTalk中,Subject和Observer接口定义于根类Object中,使得它们对所有的类都可用。
10. 代码示例
- Observer接口
一个抽象类定义了Observer接口:
Class Subject;
Class Observer{
Public:
Virtual ~Observer();
Virtual void Update(Subject* theChangedSubject) = 0;
Protected:
Observer();
};
这种实现方式支持一个观察者有多个目标。当观察者观察多个目标时,作为参数传递给Update操作的目标让观察者可以判定是哪个目标发生了改变。
类似地,下面一个抽象类定义了Subject接口
-
-
-
- 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) {
_observers->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);
}
}
-
-
-
- 具体Subject类ClockTimer
-
-
ClockTimer是一个用于存储和维护一天时间的具体目标。它每秒钟通知一次它的观察者。ClockTimer提供了一个接口用于取出单个时间单位如小时,分钟,和秒。
Class ClockTimer: public Subject{
Public:
Clock Timer();
Virtual int GetHour();
Virtual int Get Minute();
Virtual int Get Second();
Void Tick();
};
Tick操作由一个内部计时器以固定的时间间隔调用,从而提供一个精准的时间基准。Tick更新ClockTimer的内部状态并调用Notify通知观察者。实现如下,
Void ClockTimer::Tick() {
//update internal time-keeping state
//…
Notify();
}
现在我们可以定义一个DigitalClock类来显示数字时间。它从一个用户界面工具箱提供的Widget类继承了它的图形功能。通过继承Observer,Observer接口被融入DigitalClock的接口。
-
-
-
- 具体Observer类DigitalClock
-
-
Class DigitalClock: public Widget, public observer {
Public:
DigitalClock(ClockTimer*);
Virtual ~DigitalClock();
Virtual void Update(Subject*);
Virtual void Draw();
Private:
ClockTimer* _subject;
};
DigitalClock::DigitalClock(ClockTimer* s) {
_subject = s;
_subject->Attach(this);
};
DigitalClock::~DigitalClock() {
_subject->Detach(this);
};
在Update操作画出时钟图形之前,它进行检查,以保证发出通知的目标是该时钟的目标:
Void DigitalClock::Update(Subject* theChangedSubject){
If(theChangedSubject == _subject) {
Draw();
}
};
Void DigitalClock::Draw() {
//get the new value from subject
Int hour = _subject->GetHour();
Int minute = _subject->GetMinute();
// etc.
//Draw the digital Clock
};
一个Analogclock可以用类似的方法定义。
Class AnalogClock: public Widget, public Observer {
Public:
Analogclock(ClockTimer*);
Virtual void Update(Subject*);
Virtual void Draw();
//…
};
-
-
-
- 程序应用
-
-
下面的代码创建一个AnalogClock和一个DigitalClock,它们总是显示相同的时间:
ClockTimer* timer = new ClockTimer;
AnalogClock* analogClock = new AnalogClock(timer);
DigitalClock* digitalClock = new DigitalClock(timer);
一旦timer走动,两个时钟都会被更新并正确的重新显示。
11 。已知应用
最著名的Observer模式的例子出现在SmallTalk的Model/View/Controller(MVC)结构中,它是SamllTalk环境中的用户界面框架。MVC的Model类担任目标的角色,而View是观察者的基类。
12. 相关模式
中介模式Mediator:通过封装复杂的语义,ChangeMannager充当目标和观察者之间的中介者。
单例模式Singleton:ChangeManager可使用Singleton模式来保证它是唯一的并且是可全局访问的。