定义

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

结构和说明

行为型模式-观察者模式 - 图1

示例代码

  1. public class ObserverDemo {
  2. /**
  3. * 目标对象,它知道它的观察者,并提供注册和删除观察者的接口
  4. */
  5. public static class Subject {
  6. /**
  7. * 用来保存注册的观察者对象
  8. */
  9. protected List<Observer> observerList = new ArrayList<>();
  10. /**
  11. * 注册观察者对象
  12. *
  13. * @param observer 观察者对象
  14. */
  15. public void attach(Observer observer) {
  16. observerList.add(observer);
  17. }
  18. /**
  19. * 删除观察者对象
  20. *
  21. * @param observer 观察者对象
  22. */
  23. public void detach(Observer observer) {
  24. observerList.remove(observer);
  25. }
  26. /**
  27. * 通知所有注册的观察者对象
  28. */
  29. protected void notefiyObservers() {
  30. observerList.parallelStream().forEach(observer -> {
  31. observer.update(this);
  32. });
  33. }
  34. }
  35. /**
  36. * 具体的目标对象,负责把有关状态存入到相应的观察者对象
  37. */
  38. public static class ConcreteSubject extends Subject {
  39. /**
  40. * 示意,目标对象的状态
  41. */
  42. @Getter
  43. private String subjectState;
  44. public void setSubjectState(String subjectState) {
  45. this.subjectState = subjectState;
  46. // 状态发生了改变,痛着各个观察者
  47. this.notefiyObservers();
  48. }
  49. }
  50. /**
  51. * 观察者接口,定义一个更新的接口在那些目标发生改变的时候被痛着的对象
  52. */
  53. public static interface Observer {
  54. /**
  55. * 更新的接口
  56. *
  57. * @param subject 传入目标对象,方便获取相应的目标对象的状态
  58. */
  59. void update(Subject subject);
  60. }
  61. /**
  62. * 具体的观察者对象,实现更新的方法,使自身的状态和目标的状态保持一致
  63. */
  64. public static class ConcreteObserver implements Observer {
  65. /**
  66. * 示意,观察者的状态
  67. */
  68. private String observerState;
  69. @Override
  70. public void update(Subject subject) {
  71. // 具体的更新实现
  72. // 这里可能需要更新观察者的状态,使其与目标的状态保持一致
  73. ConcreteSubject concreteSubject = (ConcreteSubject) subject;
  74. this.observerState = concreteSubject.getSubjectState();
  75. }
  76. }
  77. }

命名建议

  • 观察者模式又被称为发布-订阅模式。
  • 目标接口的定义,建议在名称后面跟 Subject。
  • 观察者接口的定义,建议在名称后面跟 Observer。
  • 观察者接口的更新方法,建议名称为 update,当然方法的参数可以根据需要定义,参数个数不限、参数类型不限。

调用顺序

准备阶段示意图

行为型模式-观察者模式 - 图2

运行阶段示意图

行为型模式-观察者模式 - 图3

推模型和拉模型

  • 推模型

目标对象主动向观察者推送目标的详细信息,不管观察者是否需要,推送的信息通常是目标对象的全部或部分数据,相当于在广播通信。

  • 拉模型

目标对象在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到目标对象获取,相当于是观察者从目标对象中拉数据。一般这种模型的实现中,会把目标对象自身通过 update 方法传递给观察者,这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。

比较

  • 推模型是假定目标对象知道观察者对象需要的数据,而拉模型是目标对象不知道观察者具体需要什么数据,没有办法的情况爱,干脆把自身传递给观察者,让观察者自己去按需取值。
  • 推模型可能会使得观察者对象难以复用,因为观察者定义的 update 方法是按需而定义的,可能无法兼顾没有考虑到的情况。这就意味着出现新情况的时候,就可能需要提供新的 update 方法,或者干脆重新实现观察者。

Java 中的观察者模式

改变

  • 不需要定义观察者和目标的接口了,JDK 帮忙定义了。
  • 具体的目标实现里面不需要再维护观察者的注册信息了,这个在 Java 中的 Observable 类里面,已经帮忙实现好了。
  • 触发通知的方式有一点变化,要先调用 setChanged 方法,这个是 Java 为了帮忙实现更精确的触发控制而提供的功能。
  • 具体观察者的实现里面, update 方法其实能同时支持推模型和拉模型,这个是 Java 在定义的时候,就已经考虑进去了。

示例代码

  1. public class JavaObserverDemo {
  2. /**
  3. * 具体的目标对象,负责把有关状态存入到相应的观察者对象
  4. */
  5. public static class ConcreteSubject extends Observable {
  6. /**
  7. * 示意,目标对象的状态
  8. */
  9. @Getter
  10. private String subjectState;
  11. public void setSubjectState(String subjectState) {
  12. this.subjectState = subjectState;
  13. // 注意在用 java 中的 observer 模式的时候,下面这句话不可少
  14. this.setChanged();
  15. // 状态发生了改变,通知各个观察者
  16. this.notifyObservers();
  17. }
  18. }
  19. /**
  20. * 具体的观察者对象,实现更新的方法,使自身的状态和目标的状态保持一致
  21. */
  22. public static class ConcreteObserver implements Observer {
  23. /**
  24. * 示意,观察者的状态
  25. */
  26. private String observerState;
  27. @Override
  28. public void update(Observable o, Object arg) {
  29. // 具体的更新实现
  30. // 这里可能需要更新观察者的状态,使其与目标的状态保持一致
  31. ConcreteSubject concreteSubject = (ConcreteSubject) o;
  32. this.observerState = concreteSubject.getSubjectState();
  33. System.out.println(observerState);
  34. }
  35. }
  36. public static class Client {
  37. public static void main(String[] args) {
  38. ConcreteSubject subject = new ConcreteSubject();
  39. ConcreteObserver observer1 = new ConcreteObserver();
  40. subject.addObserver(observer1);
  41. ConcreteObserver observer2 = new ConcreteObserver();
  42. subject.addObserver(observer2);
  43. subject.setSubjectState("hello, world");
  44. }
  45. }
  46. }

优缺点

优点

  • 观察者模式实现了观察者和目标之间的抽象耦合

原本目标对象在状态发生改变的时候,需要直接调用所有的观察者对象,但是抽象出观察者接口后,目标和观察者就只是在抽象层面耦合了,也就是说目标只是知道观察者接口,并不知道具体的观察者类,从而实现目标类和具体的观察者类之间解耦。

  • 观察者模式实现了动态联动

所谓联动,就是做一个操作会引起其他相关的操作。由于观察者模式对观察者注册实行管理,那就可以在运行期间,通过动态地控制注册的观察者,来控制某个动作的联动范围,从而实现动态联动。

  • 观察者模式支持广播通信

由于目标发送通知给观察者是面向所有注册的观察者,所以每次目标通知的信息就要对所有注册的观察者进行广播。当然,也是可以在目标上添加新的功能来限制广播的范围。
在广播通信的时候要注意一个问题,就是相互广播造成死循环的问题。比如 A 和 B 两个对象互为观察者和目标对象, A 对象发生状态改变,然后 A 来广播信息,B 对象接口到通知后,在处理过程中,使得 B 对象的状态也发生了变化,然后 B 来广播信息,然后 A 对象接到通知后,又触发广播信息…,如此 A 引起 B 变化, B 又引起 A 变化,从而一直相互广播信息,就造成死循环。

缺点

  • 可能会引起无谓的操作

由于观察者模式每次都是广播通信,不管观察者需要不要,每个观察者都会被调用 update 方法,如果观察者不需要执行相应处理,那么这次操作就浪费了。其实浪费了还好,最怕引起误更新,那就麻烦了,比如,本应该在执行这次状态更新前把某个观察者删除掉,这样通知的时候就没有这个观察者了,但是现在忘掉了,那么就会引起误操作。

思考

本质

触发联动

何时选用

  • 当一个抽象模型有两个方面,其中一个方面的操作依赖于另外一个方面的状态变化,那么就可以选用观察者模式,将这两者封装成观察者和目标对象,当目标对象变化时,依赖它的观察者对象也会发生相应的变化。这样就把抽象模型的这两个方面分离开了,使得它们可以独立地改变和复用。
  • 如果在更改一个对象的时候,需要同时连带改变其他对象,而且不知道究竟应该有多少对象需要被连带改变,这种情况可以选用观察者模式,被更改的那一个对象很明显就相当于是目标对象,而需要连带修改的多个其他对象,就作为多个观察者对象了。
  • 当一个对象必须要通知其他的对象,但是你又希望这个对象和其他被通知的对象是松散耦合的。也就是说这个对象其实不想知道具体被通知的对象。这种情况可以选用观察者模式,这个对象就相当于是目标对象,而被它通知的对象就是观察者对象了。

相关模式

  • 观察者模式和状态模式

观察者模式和状态模式是有相似之处的。
观察者模式是当目标状态发生改变时,触发并通知观察者,让观察者去执行相应操作。而状态模式是根据不同的状态,选择不同的实现,这个实现类的主要功能就是针对状态相应地操作,它不像观察者,观察者本身还有很多其他的功能,接收通知并执行相应处理只是观察者的部分功能。
当然观察者模式和状态模式是可以结合使用的。观察者模式的重心在于触发联动,但是到底决定是哪些观察者会被联动,这时就可以采用状态模式来实现了,也可以采用策略模式来进行选择需要联动的观察者。

  • 观察者模式和中介者模式

观察者模式和中介者模式是可以结合使用的。
前面的例子中目标只是简单地通知一下,然后让各个观察者自己去完成更新就结束了。如果观察者和被观察的目标之间交互关系很负责,比如,有一个界面,里面有三个下拉列表组件,分别是选择国家、省份/州、具体的城市,很明显这是一个三级联动,当你选择一个国家的时候,省份/州应该相应改变数据,省份/州一改变,具体的城市也需要改变。
这种情况下,很明显需要相关的状态都联动准备好了,然后再一次性地通知观察者。也就是界面做更新处理,不会仅国家改变一下,省份和城市还没改,就通知界面更新,这种情况就可以使用中介者模式来封装观察者和目标的关系。在 Swing 的小型应用里面,也可以使用中介者模式,比如,把一个界面所有的事件用一个对象来处理,把一个组件触发事件以后,需要操作其他组件的动作都封装到一起,这个对象就是典型的中介者。