观察者模式学习

观察者模式(Observer Design Pattern)也被称为发布订阅模式(Publish-Subscribe Design Pattern)。在 GoF 的《设计模式》一书中,它的定义是这样的:

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

中文翻译:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。不过,在实际的项目开发中,这两种对象的称呼是比较灵活的,有各种不同的叫法,比如:Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener。不管怎么称呼,只要应用场景符合刚刚给出的定义,都可以看作观察者模式。

从定义中可以知道,观察者主要应用于通知的场景,并且通知的对象可能需要频繁的改变。它可以解耦被观察者与观察者。

观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、RSS Feeds,本质上都是观察者模式。

经典实现方式

我们先来看其中最经典的一种实现方式。这也是在讲到这种模式的时候,很多书籍或资料给出的最常见的实现方式。具体的代码如下所示:

java 复制代码
public interface Subject {
  void registerObserver(Observer observer);
  void removeObserver(Observer observer);
  void notifyObservers(Message message);
}

public interface Observer {
  void update(Message message);
}

public class ConcreteSubject implements Subject {
  private List<Observer> observers = new ArrayList<Observer>();

  @Override
  public void registerObserver(Observer observer) {
    observers.add(observer);
  }

  @Override
  public void removeObserver(Observer observer) {
    observers.remove(observer);
  }

  @Override
  public void notifyObservers(Message message) {
    for (Observer observer : observers) {
      observer.update(message);
    }
  }

}

public class ConcreteObserverOne implements Observer {
  @Override
  public void update(Message message) {
    //TODO: 获取消息通知,执行自己的逻辑...
    System.out.println("ConcreteObserverOne is notified.");
  }
}

public class ConcreteObserverTwo implements Observer {
  @Override
  public void update(Message message) {
    //TODO: 获取消息通知,执行自己的逻辑...
    System.out.println("ConcreteObserverTwo is notified.");
  }
}

public class Demo {
  public static void main(String[] args) {
    ConcreteSubject subject = new ConcreteSubject();
    subject.registerObserver(new ConcreteObserverOne());
    subject.registerObserver(new ConcreteObserverTwo());
    subject.notifyObservers(new Message());
  }
}

这个例子很好理解:

它具有一个被观察者接口(Subject)和一个观察者接口(Observer);

所有具体的被观察者都需要实现Subject接口,即所有的被观察者都具有注册观察者方法(registerObserver)、删除观察者方法(removeObserver)、通知观察者方法(notifyObservers);

所有具体的观察者都需要实现Observer接口,即所有的观察者都具有执行通知逻辑的方法(update);

在被观察者中通过一个List来保存所有注册的观察者,在需要时通过遍历List来通知所有的观察者。

但上面的代码算是观察者模式的"模板代码",只能反映大体的设计思路。在真实的软件开发中,并不需要照搬上面的模板代码。而且Java中也有一些第三方框架供我们使用,它们提供了实现观察者模式的骨架代码,我们可以基于此框架,非常容易地在自己的业务场景中实现观察者模式,不需要从零开始开发,如Guava 的EventBus、Spring Event等。

观察者模式的实现原理分析

如何通知观察者?

我们看到,被观察者需要通知每一个观察者,所以上面示例中被观察者通过一个List来保存了观察者,然后通过遍历List来实现通知每一个观察者。当然通过其他的容器也可以实现,比如Guava的EventBus就是用ConcurrentMap来存储的观察者信息的。

还有被观察者中保存的是观察者的接口,并不知道观察者的细节,即被观察者和观察者之间用松耦合方式结合(loosecoupling),符合"为交互对象之间的松耦合设计而努力"的设计原则。

在上面示例中是被观察者主动将消息推送到观察者中的,这种方式被称为推(push)。还有一种称为拉(pull)的方式,即被观察者通知观察者后,观察者再从被观察者中获取所需的数据。pull方式的前提条件是,被观察者需要提供对外获取数据的接口,同时观察者中需要能获取到被观察者对象。显然,pull方式使被观察者与观察者联系更紧密了,这不一定是一个好的现象

如何更具有复用性?

被观察者需要提供注册、通知等通用接口,其实这些操作和我们实际业务关系不大,可以考虑抽取出来。此时有两种可以考虑的方式,

一种是继承方式,将通用的操作都抽取到父类中,所有的被观察者只需要继承父类就可以具有注册、通知等操作的实现,JDK1.0中的观察者模式就是这样做的,它提供了Observable类用于给被观察者继承。

一种是组合方式,将通用的操作都抽取到其他类中,被观察者通过委托该类来实现注册、通知等通用操作。如果使用Guava的EventBus,就是通过组合EventBus类来实现的。

我们都知道组合的方式优于继承,因为继承有诸多限制,比如单继承。所以一般都推荐使用组合的方式,目前常用的第三方框架采用的都是组合的方式。

同步通知与异步通知

在通知观察者的方式上我们可以有两种方式,同步阻塞的实现方式和异步非阻塞的实现方式。

同步阻塞的实现方式

观察者和被观察者代码在同一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成之后,才执行后续的代码。

上面讲到的用户注册的例子就是同步阻塞方式,register() 函数依次调用执行每个观察者的 handleRegSuccess() 函数,等到都执行完成之后,才会返回结果给客户端。

异步非阻塞的实现方式

如果注册接口是一个调用比较频繁的接口,对性能非常敏感,希望接口的响应时间尽可能短,那我们可以将同步阻塞的实现方式改为异步非阻塞的实现方式,以此来减少响应时间。

实现异步非阻塞的方式我们既可以自己手动创建线程来实现,也可以采用第三方框架提供的异步执行功能,如如Guava 的EventBus、Spring Event等。

跨进程间的调用

我们一般所介绍的观察者模式都是指在一个进程间(通常就是指同一个项目代码)进行的通知,但其实观察者模式也可以应用于进程间,比如如果用户注册成功之后,我们需要发送用户信息给大数据征信系统,而大数据征信系统是一个独立的系统,跟它之间的交互是跨不同进程的。

要实现跨进程的观察者模式,其实就是要实现进程间的通讯,这个我们就比较熟悉了,常用的就是:

RPC 接口调用的方式。

消息队列的方式。(采用消息队列的发布-订阅模式)

消息队列方式相对于RPC方式来说,被观察者和观察者解耦更加彻底,两部分的耦合更小,因为被观察者完全不感知观察者,同理,观察者也完全不感知被观察者。不过弊端就是需要引入一个新的系统(消息队列),增加了维护成本。

相关推荐
西岸行者12 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意12 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码12 天前
嵌入式学习路线
学习
毛小茛12 天前
计算机系统概论——校验码
学习
babe小鑫12 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms12 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下12 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。12 天前
2026.2.25监控学习
学习
im_AMBER12 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J12 天前
从“Hello World“ 开始 C++
c语言·c++·学习