5.7 OBSERVER(观察者) —对象行为型模式

1 意图

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

2 别名

依赖(Dependents),发布-订阅(Publish-Subscribe)

3 动机

将一个系统分割成一系列相互协作的类有一个常见的副作用:需要维护相关对象间的一致性,我们不希望为了维持一致性而使各类紧密耦合,因为这样降低了它们的可重用性。
例如,许多图形用户界面工具箱将用户应用的界面表示与底下的应用数据分离。定义应用数据的类和负责界面表示的类可以各自独立地复用。当然它们也可一起工作。一个表格对象和一个柱状图对象可使用不同的表示形式描述同一个应用数据对象的信息。表格对象和柱状图对象互相并不知道对方的存在,这样使你可以根据需要数据对象的信息。表格对象和柱状图对象互相并不知道对方的存在,这样使你可以根据需要柱状图能立即反映这一变化 , 反过来也是如此。


image.png

这一行为意味着表格对象和棒状图对象都依赖于数据对象 , 因此数据对象的任何状态改变都应立即通知它们。同时也没有理由将依赖于该数据对象的对象的数目限定为两个, 对相同的数据可以有任意数目的不同用户界面。
Observer模式描述了如何建立这种关系。这一模式中的关键对象是目标(Subject)和观察者(Observer)。一个目标可以有任意数目的依赖它的观察者。一旦目标的状态发生改变 , 所有的观察者都得到通知。作为对这个通知的响应,每个观察者都将查询目标以使其状态与目标的状态同步。
这种交互也称为发布-订阅(Publish-Subscribe),目标是通知的发布者。它发出通知时并不需知道谁是它的观察者。可以有任意数目的观察者订阅并接收通知。

4 适用性

在以下任一情况下可以使用观察者模式 :

  • 1 当一个抽象模型有两个方面 , 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用;
  • 2 当对一个对象的改变需要同时改变其它对象 , 而不知道具体有多少对象有待改变;
  • 3 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之 , 你不希望这些对象是紧密耦合的。
5 结构
image.png
6 参与者
  • Subject(目标)
    ——目标知道它的观察者。可以有任意多个观察者观察同一个目标
    ——提供注册和删除观察者对象的接口
  • Observer(观察者)
    ——为那些在目标发生改变时需获得通知的对象定义一个更新接口
  • ConcreteSubject(具体目标)
    ——将有关状态存入各ConcreteObserver对象
    ——当它的状态发生变化时,向它的各个观察者发出通知
  • ConcreteObserver(具体观察者)
    ——维护一个指向ConcreteSubject对象的引用
    ——存储有关状态,这些状态应与目标的状态保持一致
    ——实现Observer的更新接口以使自身状态与目标的状态保持一致
7 协作
  • 当ConcreteSubject发生任何可能导致其观察者与其本身状态不一致的改变时,它将通知它的各个观察者;
  • 在得到一个具体目标的改变通知后,ConcreteObserver对象可向目标对象查询信息,ConcreteObserver使用这些信息以使它的状态与目标对象的状态一致。
    下面的交互图说明了一个目标对象和两个观察者之间的协作:


    image.png

    注意发出改变请求的observer对象并不立即更新,而是将其推迟到它的目标得到一个通知之后。Notify不总是由目标对象调用。它也可被一个观察者或其它对象调用。实现一节将讨论一些常用的变化。

8 效果

Observer模式允许你独立的改变目标和观察者。你可以单独复用目标对象而无需同时复用其观察者, 反之亦然。它也使你可以在不改动目标和其他的观察者的前提下增加观察者。
下面是观察者模式其它一些优缺点 :

  • 1 目标和观察者间的抽象耦合:一个目标所知道的仅仅是它有一系列观察者 , 每个都符合抽象的Observer类的简单接口。目标不知道任何一个观察者属于哪一个具体的类。这样目标和观察者之间的耦合是抽象的和最小的。
    因为目标和观察者不是紧密耦合的 , 它们可以属于一个系统中的不同抽象层次。一个处于较低层次的目标对象可与一个处于较高层次的观察者通信并通知它 , 这样就保持了系统层次的完整。如果目标和观察者混在一块 , 那么得到的对象要么横贯两个层次 (违反了层次性), 要么必须放在这两层的某一层中(这可能会损害层次抽象)。
  • 2 支持广播通信 :不像通常的请求, 目标发送的通知不需指定它的接收者。通知被自动广播给所有已向该目标对象登记的有关对象。目标对象并不关心到底有多少对象对自己感兴趣;它唯一的责任就是通知它的各观察者。这给了你在任何时刻增加和删除观察者的自由。处理还是忽略一个通知取决于观察者。
  • 3 意外的更新:因为一个观察者并不知道其它观察者的存在 , 它可能对改变目标的最终代价一无所知。在目标上一个看似无害的的操作可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新。此外 , 如果依赖准则的定义或维护不当,常常会引起错误的更新 , 这种错误通常很难捕捉。
    简单的更新协议不提供具体细节说明目标中什么被改变了 , 这就使得上述问题更加严重。如果没有其他协议帮助观察者发现什么发生了改变,它们可能会被迫尽力减少改变。
9 实现

这一节讨论一些与实现依赖机制相关的问题。

  • 1 创建目标到其观察者之间的映射:一个目标对象跟踪它应通知的观察者的最简单的方法是显式地在目标中保存对它们的引用。然而 , 当目标很多而观察者较少时 , 这样存储可能代价太高。一个解决办法是用时间换空间 , 用一个关联查找机制 (例如一个hash表)来维护目标到观察者的映射。这样一个没有观察者的目标就不产生存储开销。但另一方面 , 这一方法增加了访问观察者的开销。
  • 2 观察多个目标:在某些情况下, 一个观察者依赖于多个目标可能是有意义的。例如 , 一个表格对象可能依赖于多个数据源。在这种情况下 , 必须扩展Update接口以使观察者知道是哪一个目标送来的通知。目标对象可以简单地将自己作为Update操作的一个参数, 让观察者知道应去检查哪一个目标。
  • 3 谁触发更新:目标和它的观察者依赖于通知机制来保持一致。但到底哪一个对象调用Notify来触发更新? 此时有两个选择:
    • 1 由目标对象的状态设定操作在改变目标对象的状态后自动调用Notify。这种方法的优点是客户不需要记住要在目标对象上调用 Notify,缺点是多个连续的操作会产生多次连续的更新, 可能效率较低。
    • 2 让客户负责在适当的时候调用Notify。这样做的优点是客户可以在一系列的状态改变完成后再一次性地触发更新 ,避免了不必要的中间更新。缺点是给客户增加了触发更新的责任。由于客户可能会忘记调用Notify,这种方式较易出错。
  • 4 对已删除目标的悬挂引用:删除一个目标时应注意不要在其观察者中遗留对该目标的悬挂引用。一种避免悬挂引用的方法是 , 当一个目标被删除时,让它通知它的观察者将对该目标的引用复位。一般来说, 不能简单地删除观察者, 因为其他的对象可能会引用它们, 或者也可能它们还在观察其他的目标。
  • 5 在发出通知前确保目标的状态自身是一致的:在发出通知前确保状态自身一致这一点很重要, 因为观察者在更新其状态的过程中需要查询目标的当前状态。
    当Subject的子类调用继承的该项操作时 , 很容易无意中违反这条自身一致的准则。例如 ,下面的代码序列中, 在目标尚处于一种不一致的状态时,通知就被触发了 :
    image.png
  • 6 避免特定于观察者的更新协议 — 推/拉模型:观察者模式的实现经常需要让目标广播关于其改变的其他一些信息。目标将这些信息作为Update操作一个参数传递出去;这些信息的量可能很小也可能很大。
    一个极端情况是,目标向观察者发送关于改变的详细信息 , 而不管它们需要与否。我们称之为推模型(push model)。另一个极端是拉模型(pull model); 目标除最小通知外什么也不送出,而在此之后由观察者显式地向目标询问细节。
    拉模型强调的是目标不知道它的观察者 , 而推模型假定目标知道一些观察者的需要的信息。推模型可能使得观察者相对难以复用,因为目标对观察者的假定可能并不总是正确的。另一方面。拉模型可能效率较差 , 因为观察者对象需在没有目标对象帮助的情况下确定什么改变了。
  • 7 显式地指定感兴趣的改变:你可以扩展目标的注册接口 ,让各观察者注册为仅对特定事件感兴趣,以提高更新的效率。当一个事件发生时 , 目标仅通知那些已注册为对该事件感兴趣的观察者。支持这种做法一种途径是,对使用目标对象的方面(aspects)的概念。可用如下代码将观察者对象注册为对目标对象的某特定事件感兴趣:
    void Subject::Attach(Observer, Aspect& interest);
    此处interest指定感兴趣的事件。在通知的时刻 , 目标将这方面的改变作为Update操作的一个参数提供给它的观察者,例如:
    void Observer::Update(Subject
    , Aspect& interest);
  • 8 封装复杂的更新语义:当目标和观察者间的依赖关系特别复杂时 , 可能需要一个维护这些关系的对象。我们称这样的对象为更改管理器(ChangeManager)。它的目的是尽量减少观察者反映其目标的状态变化所需的工作量。例如 , 如果一个操作涉及到对几个相互依赖的目标进行改动, 就必须保证仅在所有的目标都已更改完毕后,才一次性地通知它们的观察者 ,而不是每个目标都通知观察者。
    ChangeManager有三个责任:
    • 1 它将一个目标映射到它的观察者并提供一个接口来维护这个映射。这就不需要由目标来维护对其观察者的引用, 反之亦然。
    • 2 它定义一个特定的更新策略。
    • 3 根据一个目标的请求, 它更新所有依赖于这个目标的观察者。
      下页的框图描述了一个简单的基于ChangeManager的Observer模式的实现。有两种特殊的ChangeManager。SimpleChangeManager总是更新每一个目标的所有观察者 , 比较简单。相反,DAGChangeManager处理目标及其观察者之间依赖关系构成的无环有向图。当一个观察者观察多个目标时, DAGChangeManager要比SimpleChangeManager更好一些。在这种情况下, 两个或更多个目标中产生的改变可能会产生冗余的更新。DAGChangeManager保证观察者仅接收一个更新。当然,当不存在多重更新的问题时 , SimpleChangeManager更好一些。
      ChangeManager是一个Mediator(5.5)模式的实例。通常只有ChangeManager, 并且它是全局可见的。这里Singleton(3.5)可能有用。
  • 9 结合目标类和观察者类 用不支持多重继承的语言(如Smalltalk )书写的类库通常不单独定义Subject和Observer类,而是将它们的接口结合到一个类中。这就允许你定义一个既是一个目标又是一个观察者的对象,而不需要多重继承。
    image.png
10 代码实例

github地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,326评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,228评论 1 304
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,979评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,489评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,894评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,900评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,075评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,803评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,565评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,778评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,255评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,582评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,254评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,151评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,952评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,035评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,839评论 2 277

推荐阅读更多精彩内容