观察者模式( Observer Pattern ): MVC的进一步泛化

参考书籍:

  1. 《Design Patterns: Elements of Reusable Object-Oriented Software》

在商业项目中第一次读到观察者模式的代码是一个异步服务器消息分发的实现部分。由此开始学习了一下 。

设计模式用前须知

  • 设计模式中一句出现频率非常高的话是,“ 在不改动。。。。的情况下, 实现。。。。的扩展“ 。
  • 对于设计模式的学习者来说,充分思考这句话其实非常重要, 因为这句往往只对框架/ 工具包的设计才有真正的意义。因为框架和工具包存在的意义,就是为了让其他的程序员予以利用, 进行功能的扩展,而这种功能的扩展必须以不需要改动框架和工具包中代码为前提
  • 对于应用程序的编写者, 从理论上来说, 所有的应用层级代码至少都是处于可编辑范围内的, 如果不细加考量, 就盲目使用较为复杂的设计模式, 反而会得不偿失, 毕竟灵活性的获得, 也是有代价的。

观察者模式与MVC

如果说单例模式工厂方法模式是入门级程序员都必须掌握的方法, 那么观察者模式则是所有实际项目开发者都会直接或者间接用到的模式, 原因是:

  • 观察者模式是MVC模式的一种更为泛化的描述(引自参考书籍1)

    • MVC 模式中的Model 其实就是观察者模式中的被观察者(Subject), Model的状态变化, 都会引发View 层次的变动,而MVC的View层次自然就对应观察者模式中的观察者(Observer)
  • 观察者模式顾名思义, 主要有两种角色:

    • 观察者(Observer)
    • 观察对象/被观察者(Subject/ Observable)

观察者模式( Observer Pattern ): MVC的进一步泛化_第1张图片

图例说明


在这里插入图片描述
在这里插入图片描述
这种关系在 java 语言中, 可以按照如下形式实现(不止这一种实现形式)

abstract class A {
    private B b;
}

在这里插入图片描述
这种关系在 java 语言中, 可以按照如下形式实现(不止这一种实现形式)

abstract class A {
    private List<B> b;
}

注意: A, B 的类型比较灵活, 可以是 class, abstract class , interface 的任意一种类型, 使用哪种完全看需求


观察者模式( Observer Pattern ): MVC的进一步泛化_第2张图片
这种关系在 java 语言中, 可以按照如下形式实现

class A extends B{
}

class A implements B{
}

观察者模式( Observer Pattern ): MVC的进一步泛化_第3张图片

观察者模式解析

上面的结构图省略了一个对初学者而言很重要的信息,观察者与观察对象的关联是何时建立的。

  • 注意到观察对象(Subject)定义了Attach(Observer), Detach(Observer) 方法, 但这里隐含的一个问题是,是观察者来调用Attach/Detach 方法, 还是被观察者调用 Attach Detach 方法 ?
    • 稍微思考一下可以得出答案, 是否观察某一个对象应该有观察者来决定, 而不是被观察者决定, 所以Attach/Detach 方法应该由Observer 来调用。

那么紧接着的第二个问题是图例中Observer 并没有 Subject 的引用, 如何调用其Attach/Detach 方法

  • 答案一: Subject 是具有全局可访问性(例如public class 的 public static 成员)
  • 答案二: 在Observer 被构造的时候传入Subject 对象, 自此之后, Observer 一直持有Subject的引用。

观察者模式在下列情形时应当被使用:

  • 当对一个对象的的改动会引发其他对象的变动时,而且你无法预测有多少个对象需要被改动。
  • 当一个对象需要有能力通知其他对象, 且不需要了解这些对象是什么类型时。

观察者模式在实现时,需要考虑的一些问题

  • 问题1: 被观察者与观察者对应关系的维护方式

    • 最简单也是最常见的方法是由被观察者保存需要通知的观察者 的引用, 然后像之前的图例中的伪代码所展示的一样, 通过一个For循环来触发对应观察者的Update()方法。
      • 但这种方式的缺点是, 当观察者比较多,而被观察者比较少时,大量的观察者引用可能会被重复保存,引发比较大的存储开销。
    • 另一种可选的方式是关联查询, 如利用 HashTable 维护观察者与被观察者的对应关系
    • 这种方式避免了引用关系重复保存的缺点
    • 缺点是增加了被观察者访问观察者模式的开销(需要先查HashTable,再访问)
  • 问题2: 当一个观察者需要观察不止一个被观察者时,当收到一个通知时, 如何知道是哪一个被观察者发来了通知。

    • 这种情况,可以简单的通过在Update()方法中, 把被观察者的引用作为参数传入,以此让观察者知道是哪一个被观察者调用了Notify()方法。
  • 问题3: 何时调用Notify方法()

    • 第一个选择是把Notify 动作嵌入到 被观察者实现类的SetState方法中, 这样保证了每一次被观察者状态的改变都会被通知给所有的观察者
      • 缺点是一个操作有可能分为很多步, 其中有多次调用SetState方法,而其实只有最后一次SetState完成时才需要通知其他观察者, 重复地调用SetState 而引发的多次Update可能会比较低效。
    • 另一种选择是由修改被观察者状态的对象来决定调用Notify 方法的时刻
      • 好处是避免了在一些中间步骤中触发Update方法, 缺点是把调用Notify方法的任务交给了被观察者的改动方, 他们有可能忘记调用Notify方法。
  • 问题4: 当一个被观察者被删除时, 应该对相应的观察者作何处理。

    • 删除一个被观察者时, 不应该导致对该观察者无效的引用产生, 比较何时的作法时, 当一个被观察者删除时, 需要通知所有观察者, 以期观察者做出何时的处理。
  • 问题5: 如何保证观察者对象在调用Notify方法之前,自身的状态处于一个正常的状态,而不是修改到一半的状态。

    • 这个问题乍一看有些奇怪且不必要,但其实在被观察者存在继承关系时,很容易出现。 下面通过Java代码的例子进行说明。
// 被观察者基类
public class BaseSubject {
    private int value = 0;
    public void operation(int newValue)
    {
        value = 1;
        notifyObservers()
    }
    public void notify()
    {
		...
	}
}

//被观察者子类
public class MySubject extends BaseSubject {
    private int myValue = 2;
    public void myOperation( int newValue)
    {
        super.operation(newValue); // 这个操作已经触发了notify 方法
        this.myValue += newValue;
        .... // 后续的对被观察者子对象的状态修改尚未被通知给观察者,除非最后显示调用了notifyObservers() 方法

    }
}

这个问题可以通过设计模式之模板方法(Template Method) 模式避免。 具体方案是定义一个元操作供子类重写, 然后在模板方法中将 notify () 操作放在模板方法中最后进行的操作,以此来保证当观察者子类重写观察者基类时, 不会遗漏Notify 操作。

  • 观察者模式在实现时,需要考虑的一些问题(高级篇)
    • 问题1: 观察者模式在实现时,被观察者的状态发生变化时, 除了要通知观察者他们观察的对象已经发生变化, 往往还需要向观察者发一些额外的信息, 这里有两种选择
      • 推模型 (push model )
        • 被观察者会把尽可能多的信息包含在Update 方法中, 尽管被通知的观察者可能并不需要其中的某些信息。
          • 推模型的缺点在于默认了被观察者了解哪些是观察者所需要的内容, 这有可能使得被观察者的重用性降低, 因为如果有新的信息要添加到Update方法中时, 或有内容要从Update方法中删除时,都会很不方便,因为有很多依赖于该方法的观察者。
      • 拉模型
        • 被观察者会在Update方法中包含最少的信息, 如果观察者还需要额外的信息, 可以自行通过别的方法来获取。
          • 拉模型的好处在于默认观察者不了解观察者所需要的信息, 因此特定类别的观察者需要增加获取的信息时, 只需要修改特定的获取信息的方法即可。
    • 问题2: 不同观察者对于同一个被观察者的状态改变的事件类别是不一样的, 可能A类观察者只关心A类别事件导致的状态变化, B类观察者只关心B类别事件导致的状态变化, 那么如何使得被观察者对于状态的变化进行分类别通知
      • 可以修改观察者注册接口(Attach) 方法为void attach(Observer o, Aspect interest); 使得观察者在注册时,同时告诉被观察者他关心的事件类别。 而被观察者在特定的事件发生,可以仅仅通知那些注册了该类别事件的观察者。
    • 问题3: 当一个观察者(Observer)需要观察多个被观察者(Subject)时 , 一个发生在两个或多个被观察者的改变有可能引发冗余的Update() 方法, 此时有可能会需要引入一个ChangeManager, 该ChangeManager职责如下:
      • 维护观察者到被观察者的映射关系, 这样观察者和被观察者都无须再保留相互之间的引用。
      • 定义特定的Update 策略, 当一个被观察者发出请求时, 只更新对该被观察者有依赖的观察者。

观察者模式( Observer Pattern ): MVC的进一步泛化_第4张图片

你可能感兴趣的:(设计模式)