观察者模式是另一种非常有用和流行的模式,同样应用于很多框架中,在对象状态发生变化且需要将这些变化通知其他对象的情况下使用此模式。
问题提出
假设我们设计电子表格。
我们用饼图展示单元格的值,当单元格的值变化时,饼图即时反映这种变化。同样Total也即时反映单元格的更改进行重新计算。
变化的单元格值是数据源(DataSource),饼图和总数Total必须即时了解数据源的变化。
用代码模拟。
创建类 DataSource,它有value属性。
另外创建SpreadSheet类和Chart类,分别表示电子表格和图表。
数据源并不知道有多少电子表格或图表依赖于它,并且在运行的过程中,陆续会添加其他电子表格或图表并依赖于该数据源。
如何让这些对象即时了解数据源的改变?
解决方案
目前我们创建的类如下。
我们希望DataSource与SpreadSheet、Chart这些类通信,但又不能与他们紧密耦合。
显然,DataSource应该和接口通信,我们引入一个接口Observer,而SpreadSheet和Chart实现这个接口。
这个实现是符合开放闭合原则的,明天我们如果要创建一个基于数据源DataSourcce即时生成的图像,只需创建一个实现Observer接口的新类即可。
DataSource需要维持一个Observer对象的列表,当数据源数据有变化(setValue)时,我们需要通知该列表里所有的Observer对象。利用面向对象的多态性原则,很容易实现。
我们给Observer接口定义一个方法update(),Observer接口的具体类负责实现update,或许是重画图表,或许是重新计算总和等。
再给DataSource类引入3个方法。addObserver、removeObserver、notifyObservers。addObserver和removeObserver用于动态添加删除Observer对象,notifyObservers则负责在数据发生变化时循环调用Observer对象的update方法。
这就是观察者模式(Observer Pattern)。
四人帮UML图的命名则比较抽象。
ConcreteSubject类相当于我们的DataSource类,它实现了抽象类Subject。
它与发布订阅模式很类似。
Subject类扮演的是发布者的角色,而Observer类则是事件的订阅者。
代码实现
观察者模式实现非常简单。
创建接口Observer,定义方法update()。
类SpreadSheet和Chart,均实现Observer接口。
创建类Subject,它有observers列表,addObserver方法、removeObserver方法以及notifyObservers方法。在notifyObservers方法中,遍历每个Observer对象并调用其update方法。
DataSource扩展Subject,并有一个value。在设置value时(setValue),调用Subject的notifyObservers方法更新所有Observer对象。
最后到main方法里测试。
新建DataSource对象、多个SpreadSheet对象、Chart对象,再将这些对象加到observers列表(addObserver)。
更改value(setValue),测试各Observer对象能及时作出相应的反应。
还有个问题。
目前,各个Observer对象并不知道源数据更改的内容是什么,只是知道数据源作了更改。
通信方式
下图是经典的观察者模式UML图,Observer能得到Subject改变的通知,但不知道Subject改变的内容。
一种解决方法是引入参数比如value,在我们的例子里,value是一个整数,实际应用的参数可能会是包含多个字段的复杂对象。
这种通信方式我们称之为推方式(Push Style),Subject将它的变化推送到Observer。在推方式下,具体的Observer是不会依赖具体的Subject的。
但是,明天我们可能会引入某个Observer,它希望从Subjec获取的内容是有不同字段的对象,而我们的Subject推送给Observer数据格式是固定的,所以这种方法灵活性不是很好。
我们可以使用拉方式(Pull Style)的通信方式。
拉方式下,Observer对象可以拉取它们自己需要的数据,而不是由Subject推送。每一个具体的Observer依赖于具体的Subject,可以调用各种方法获取它想要的数据,这种方式我们称之为拉方式。
这种方法让我们有更大的灵活性,因为每个具体的Observer都可以获取它想要的不同的数据。然而很明显,它们之间存在耦合。
不过,这不算坏的耦合类型。
坏的耦合类型是具体的Subject和具体的Observer之间的耦合。我们不希望我们的DataSource依赖于SpreadSheet或Chart,因为SpreadSheet或Chart是会变的,而且未来可能会添加新的Observer,我们不希望每次引进新的Observer类时都要修改DataSource或Subject,出现这种情况就是坏的耦合类型。实际上,是不存在零耦合的软件系统的,我们总是有某种耦合,但重要的是耦合的方向或者说是关系的方向。在我们这个例子里,尽管具体Observer与具体Subject之间有耦合,但耦合方向是可以接受的。
推方式
我们来看看如何实现“推方式”的通信方式。
修改Observer接口的update方法,添加参数value。SpreadSheet和Chart的update方法实现也作相应修改。
我们看到,推方式的通信方式中,SpreadSheet和Chart等Observer对象只管接收参数的某个值使用,而对DataSource一无所知,所以它们不依赖DataSource或者说不与DataSource耦合。
同样地,我们也修改Subject类的notifyObservers以及DataSource的setValue。
在我们这个例子里,推方式推送的参数value是一个整数,显然是不够灵活的,我们可以使用Object类,如定义Observer接口的update方法为:
update(Object value);
这样我们就可以传递任何类型的对象。另外一种做法是使用泛型。
这里只是为了演示说明,我们就保持使用整数(int value)。
最后到main方法测试。
我们看到,SpreadSheet和Chart等Observer对象获取了DataSource变化的数据。
拉方式
再来看看如何实现拉方式。
使用拉方式,每一个Observer类可以拉取他们自己需要的数据,我们不再需要在update方法里传递数据。我们将相关类代码里update方法的参数删除掉。
现在具体的Observer对象比如SpreadSheet需要了解具体的Subject比如DataSource,因为Spreadsheet要从DataSource中获取它想要的数据。
SpreadSheet添加对象dataSource对象,并在构造函数中初始化。在update方法的实现中,调用dataSource的getValue方法。DataSource如果有多个获取数据的方法,SpreadSheet可以只调用与它相关的方法。
这个通信方式就是拉方式。
SpreadSheet依赖DataSource,但就像前面我们说过的,这不算一个坏的依赖,这种依赖不是世界末日。
同样的,我们对Chart类也作相应的修改。
修改类Subject和DataSource的notifyObservers方法,去掉value参数,因为在拉方式下不再需要。
再到main方法里测试,结果与推方式是一样的。当然在创建SpreadSheet和Chart对象时,需要在构造函数里提供dataSource对象。
小结
观察者模式用于对象状态发生变化且需要将这些变化通知其他对象的场景,数据源与观察者之间的通信方式有两种:推方式和拉方式。