设计模式-观察者模式 Observer
观察者模式
- 一、概述
- 二、使用场景
- 三、发布订阅
- 1) 观察者模式
- 2) 发布-订阅模式
- 四、源码使用
- 1) jdk中的观察者
- 2) Guava中的消息总线
- 五、进阶
- 1) 异步非阻塞模型
一、概述
观察者模式是一种行为设计模式,允许对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新。在这种模式中,发生状态改变的对象被称为“主题”(Subject),依赖它的对象被称为“观察者”(Observer)。
观察者模式(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等等。不管怎么称呼,只要应用场景符合刚刚给出的定义,都可以看作观察者模式。
让通过一个简单的例子来实现观察者模式。假设有一个气象站(WeatherStation),需要向许多不同的显示设备(如手机App、网站、电子屏幕等)提供实时天气数据。
首先,需要创建一个Subject接口,表示主题:
public interface Subject {
/**
* 注册观察者
* @param observer
*/
void registerObserver(Observer observer);
/**
* 删除具体的观察者
* @param observer
*/
void removeObserver(Observer observer);
/**
* 一旦发生了观察的行为,就通知所有的观察者
*/
void notifyObservers();
}
接下来,创建一个Observer接口,表示观察者:
public interface Observer {
/**
* 观察的行为发生了,该方法应该被调用
* @param newTemperature 更新的温度
*/
void update(double newTemperature);
}
现在,创建一个具体的主题,如WeatherStation
,实现Subject接口:
public class WeatherStation implements Subject{
// 温度
private double temperature;
// 持有多个观察者
private final List<Observer> observerList = new ArrayList<>();
public void changeTemperature(double newTemperature) {
this.temperature = newTemperature;
notifyObservers(newTemperature);
}
@Override
public void registerObserver(Observer observer) {
observerList.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observerList.remove(observer);
}
@Override
public void notifyObservers(double newTemperature) {
for (Observer observer : observerList) {
observer.update(newTemperature);
}
}
}
最后,创建一个具体的观察者,如 AppClient
/WebClient
,实现Observer接口:
public class AppClient implements Observer{
@Override
public void update(double newTemperature) {
System.out.println("App获取最新温度:" + newTemperature);
}
}
public class WebClient implements Observer{
@Override
public void update(double newTemperature) {
System.out.println("Web获取最新温度:" + newTemperature);
}
}
现在可以创建一个WeatherStation
实例并向其注册AppClient
观察者。当WeatherStation
的数据发生变化时,AppClient
会收到通知并更新自己的显示。
public class Main {
public static void main(String[] args) {
// 定义气象站
Subject weatherStation = new WeatherStation();
// 定义观察者
Observer appClient = new AppClient();
Observer webClient = new WebClient();
// 建立监听关系
weatherStation.registerObserver(appClient);
weatherStation.registerObserver(webClient);
// 气象站更新温度
weatherStation.notifyObservers(25.4);
}
}
在这个例子中,创建了一个WeatherStation实例,并向其注册了AppClient、WebClient观察者。当WeatherStation的数据发生变化时,所有观察者都会收到通知并更新自己的显示。 这个例子展示了观察者模式的优点:
- 观察者和主题之间的解耦:主题只需要知道观察者实现了Observer接口,而无需了解具体的实现细节。
- 可以动态添加和删除观察者:通过调用registerObserver和removeObserver方法,可以在运行时添加和删除观察者。
- 主题和观察者之间的通信是自动的:当主题的状态发生变化时,观察者会自动得到通知并更新自己的状态。 观察者模式广泛应用于各种场景,例如事件处理系统、数据同步和更新通知等。学习并掌握观察者模式对于成为一个优秀的Java程序员非常有帮助。
上面的小例子算是观察者模式的“模板代码”,可以反映该模式大体的设计思路。在真实的软件开发中,并不需要照搬上面的模板代码。观察者模式的实现方法各式各样,函数、类的命名等会根据业务场景的不同有很大的差别,比如 register 函数还可以叫作 attach,remove 函数还可以叫作 detach 等等。不过,万变不离其宗,设计思路都是差不多的。
二、使用场景
以下是一些使用观察者设计模式的例子:
- 股票行情应用:股票行情应用中,当股票价格发生变化时,需要通知订阅了该股票的投资者。这里,股票价格更新可以作为被观察者,投资者可以作为观察者。当股票价格发生变化时,所有订阅了该股票的投资者都会收到通知并更新自己的投资策略。
- 网络聊天室:在网络聊天室中,当有新消息时,需要通知所有在线的用户。聊天室服务器可以作为被观察者,用户可以作为观察者。当有新消息时,聊天室服务器会通知所有在线用户更新聊天记录。
- 拍卖系统:在拍卖系统中,当出价发生变化时,需要通知所有关注该拍品的用户。这里,拍卖系统可以作为被观察者,用户可以作为观察者。当出价发生变化时,所有关注该拍品的用户都会收到通知并更新自己的出价策略。
- 订阅系统:在订阅系统中,当有新的内容发布时,需要通知所有订阅了该内容的用户。这里,内容发布可以作为被观察者,用户可以作为观察者。当有新内容发布时,所有订阅了该内容的用户都会收到通知并获取最新内容。
- 游戏中的事件系统:在游戏中,当某个事件发生时(如角色升级、道具获得等),可能需要通知多个游戏模块进行相应的处理。这里,游戏事件可以作为被观察者,游戏模块可以作为观察者。当游戏事件发生时,所有关注该事件的游戏模块都会收到通知并执行相应的逻辑。
- 运动比赛实时更新:在体育比赛中,实时更新比分、技术统计等信息对于球迷和分析师非常重要。在这种场景下,比赛数据更新可以作为被观察者,球迷和分析师可以作为观察者。当比赛数据发生变化时,所有关注比赛的球迷和分析师都会收到通知并更新数据。
- 物联网传感器系统:在物联网(IoT)系统中,有很多传感器不断地采集数据,当数据发生变化时,需要通知相关联的设备或系统。在这种场景下,传感器可以作为被观察者,关联的设备或系统可以作为观察者。当传感器数据发生变化时,所有关联的设备或系统都会收到通知并执行相应的操作。
- 电子邮件通知系统:在一个任务管理系统中,当任务的状态发生变化(如:新任务分配、任务完成等)时,需要通知相关的人员。这里,任务状态更新可以作为被观察者,相关人员可以作为观察者。当任务状态发生变化时,所有关注该任务的人员都会收到通知并查看任务详情。
- 社交网络:在社交网络中,用户关注其他用户以获取实时动态。当被关注的用户发布新动态时,需要通知所有关注者。在这种场景下,被关注的用户可以作为被观察者,关注者可以作为观察者。当被关注的用户发布新动态时,所有关注者都会收到通知并查看动态。
三、发布订阅
发布-订阅模式和观察者模式都是用于实现对象间的松耦合通信的设计模式。尽管它们具有相似之处,但它们在实现方式和使用场景上存在一些关键区别。他们在概念上有一定的相似性,都是用于实现对象间的松耦合通信。可以将发布-订阅模式看作是观察者模式的一种变体或扩展。
我分别解释一下这两种模式。
1) 观察者模式
观察者模式定义了一种一对多的依赖关系,当一个对象(被观察者)的状态发生变化时,所有依赖于它的对象(观察者)都会得到通知并自动更新。在这个模式中,被观察者和观察者之间存在直接的关联关系。观察者模式主要包括两类对象:被观察者(Subject)和观察者(Observer)
2) 发布-订阅模式
发布-订阅模式(生产者和消费者)与观察者模式类似,但它们之间有一个关键区别:发布-订阅模式引入了一个第三方组件(通常称为消息代理或事件总线),该组件负责维护发布者和订阅者之间的关系。这意味着发布者和订阅者彼此不直接通信,而是通过消息代理进行通信。这种间接通信允许发布者和订阅者在运行时动态地添加或删除,从而提高了系统的灵活性和可扩展性。
Java中的发布-订阅模式示例:
public interface Subscriber {
void onEvent(Map<String, Object> eventContextMap);
}
public class AppSubscriber implements Subscriber{
@Override
public void onEvent(Map<String, Object> eventContextMap) {
System.out.println("app -> 当前的温度是: " + eventContextMap.get("temp"));
}
}
public class WebSubscriber implements Subscriber{
@Override
public void onEvent(Map<String, Object> eventContextMap) {
System.out.println("web -> 当前的温度是: " + eventContextMap.get("temp"));
}
}
// 创建消息总线
public class EventBus {
// 维护事件(对象,字符串)和订阅者的关系
private final Map<String, List<Subscriber>> subscriberMap = new HashMap<>(8);
public void registerSubscriber(String eventType, Subscriber subscriber) {
// 通过事件类型,来确定有没有已存在订阅者
subscriberMap.computeIfAbsent(eventType, v -> new ArrayList<>());
// 获取订阅者的集合
List<Subscriber> subscriberList = subscriberMap.get(eventType);
subscriberList.add(subscriber);
// 注册
subscriberMap.put(eventType, subscriberList);
}
public void removeSubscriber(String eventType, Subscriber subscriber) {
List<Subscriber> subscriberList = subscriberMap.get(eventType);
if (subscriberList != null) {
subscriberList.remove(subscriber);
}
}
public void publishEvent(String eventType, Map<String, Object> eventContextMap) {
List<Subscriber> subscriberList = subscriberMap.get(eventType);
for (Subscriber subscriber : subscriberList) {
subscriber.onEvent(eventContextMap);
}
}
}
// 使用示例:
public class WeatherStation {
private double temperature;
private EventBus eventBus;
public WeatherStation(EventBus eventBus) {
this.eventBus = eventBus;
}
public void changeTemperature(double newTemperature) {
this.temperature = newTemperature;
Map<String, Object> eventContextMap = new HashMap<>(2);
eventContextMap.put("temp", newTemperature);
eventBus.publishEvent("changeTemperature", eventContextMap);
}
public static void main(String[] args) {
// 创建订阅者
AppSubscriber appSubscriber = new AppSubscriber();
WebSubscriber webSubscriber = new WebSubscriber();
// 构建消息总线
EventBus eventBus = new EventBus();
eventBus.registerSubscriber("changeTemperature", appSubscriber);
eventBus.registerSubscriber("changeTemperature", webSubscriber);
// 创建气象站
WeatherStation weatherStation = new WeatherStation(eventBus);
weatherStation.changeTemperature(25.6);
}
}
总结一下两者的区别:
- 通信方式:观察者模式中,观察者与被观察者之间存在直接的关联关系,而发布-订阅模式中,发布者和订阅者通过一个第三方组件(消息代理或事件总线)进行通信,彼此之间不存在直接关联关系。
- 系统复杂性:发布-订阅模式引入了一个额外的组件(消息代理或事件总线),增加了系统的复杂性,但同时也提高了系统的灵活性和可扩展性。
- 使用场景:观察者模式适用于需要将状态变化通知给其他对象的情况,而发布-订阅模式适用于事件驱动的系统,尤其是那些需要跨越多个模块或组件进行通信的场景。 希望这个解释能帮助您理解发布-订阅模式和观察者模式之间的区别。如果您有其他问题,欢迎继续提问。
发布-订阅模式和传统的观察者模式相比,在某些方面具有优势。以下是发布-订阅模式相对于观察者模式的一些优点:
- 解耦:在发布-订阅模式中,发布者和订阅者之间没有直接关联,它们通过一个中间组件(消息代理或事件总线)进行通信。这种间接通信可以使发布者和订阅者在运行时动态地添加或删除,从而进一步降低了它们之间的耦合度。
- 可扩展性:发布-订阅模式允许您更容易地向系统中添加新的发布者和订阅者,而无需修改现有的代码。这使得系统在不同组件之间通信时具有更好的可扩展性。
- 模块化:由于发布者和订阅者之间的通信通过中间组件进行,您可以将系统划分为更小、更独立的模块。这有助于提高代码的可维护性和可读性。
- 异步通信:发布-订阅模式通常支持异步消息传递,这意味着发布者和订阅者可以在不同的线程或进程中运行。这有助于提高系统的并发性能和响应能力。
- 消息过滤:在发布-订阅模式中,可以利用中间组件对消息进行过滤,使得订阅者只接收到感兴趣的消息。这可以提高系统的性能,减少不必要的通信开销。
然而,发布-订阅模式也有一些缺点,例如增加了系统的复杂性,因为引入了额外的中间组件。根据具体的应用场景和需求来选择合适的设计模式是很重要的。在某些情况下,观察者模式可能更适合,而在其他情况下,发布-订阅模式可能是更好的选择。
四、源码使用
1) jdk中的观察者
java.util.Observable类实现了主题(Subject)的功能,而java.util.Observer接口则定义了观察者(Observer)的方法。
通过调用Observable对象的notifyObservers()方法,可以通知所有注册的Observer对象,让它们更新自己的状态。
一下是一个使用案例:假设有一个银行账户类,它的余额是可变的。当余额发生变化时,需要通知所有的观察者(比如说银行客户),以便它们更新自己的显示信息。
// 银行账户类
public class BankAccount extends Observable {
private double balance;
// 构造函数
public BankAccount(double balance) {
this.balance = balance;
}
// 存款操作
public void deposit(double amount) {
balance += amount;
setChanged(); // 表示状态已经改变
notifyObservers(); // 通知所有观察者
}
// 取款操作
public void withdraw(double amount) {
balance -= amount;
setChanged(); // 表示状态已经改变
notifyObservers(); // 通知所有观察者
}
// 获取当前余额
public double getBalance() {
return balance;
}
// 主函数
public static void main(String[] args) {
BankAccount account = new BankAccount(1000.0);
// 创建观察者
Observer observer1 = new Observer() {
@Override
public void update(Observable o, Object arg) {
System.out.println("客户1: 余额已更新为 " + ((BankAccount)o).getBalance());
}
};
Observer observer2 = new Observer() {
@Override
public void update(Observable o, Object arg) {
System.out.println("客户2: 余额已更新为 " + ((BankAccount)o).getBalance());
}
};
// 注册观察者
account.addObserver(observer1);
account.addObserver(observer2);
// 存款操作,触发观察者更新
account.deposit(100.0);
// 取款操作,触发观察者更新
account.withdraw(50.0);
}
}
这个案例中,BankAccount类继承了java.util.Observable类,表示它是一个主题(Subject)。在存款或取款操作时,它会调用setChanged()方法表示状态已经改变,并调用notifyObservers()方法通知所有观察者(Observer)。
在主函数中,创建了两个观察者(observer1和observer2),它们分别实现了Observer接口的update()方法。当观察者收到更新通知时,它们会执行自己的业务逻辑,比如更新显示信息。
这个案例演示了观察者模式在银行系统中的应用,通过观察者模式可以实现银行客户对自己账户余额的实时监控。
2) Guava中的消息总线
Guava 库中的 EventBus
类提供了一个简单的消息总线实现,可以帮助在 Java 应用程序中实现发布-订阅模式。以下是一个简单的示例,演示了如何使用 Guava 的 EventBus
来实现一个简单的消息发布和订阅功能。
首先,确保您已将 Guava 添加到项目的依赖项中。如果您使用 Maven,请在 pom.xml
文件中添加以下依赖项:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
接下来,定义一个事件类,例如 MessageEvent
:
public class MessageEvent {
private String message;
public MessageEvent(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
现在,创建一个订阅者类,例如 MessageSubscriber
。在订阅者类中,定义一个方法并使用 @Subscribe
注解标记该方法,以便 EventBus
能够识别该方法作为事件处理器:
public class MessageSubscriber {
@Subscribe
public void handleMessageEvent(MessageEvent event) {
System.out.println("收到消息: " + event.getMessage());
}
}
最后,来看一个使用示例:
public class Main {
public static void main(String[] args) {
// 创建 EventBus 实例
EventBus eventBus = new EventBus();
// 创建并注册订阅者
MessageSubscriber subscriber = new MessageSubscriber();
eventBus.register(subscriber);
// 发布事件
eventBus.post(new MessageEvent("Hello, EventBus!"));
// 取消注册订阅者
eventBus.unregister(subscriber);
// 再次发布事件(此时订阅者已取消注册,将不会收到消息)
eventBus.post(new MessageEvent("Another message"));
}
}
在这个示例中,我们创建了一个 EventBus
实例,然后创建并注册了一个 MessageSubscriber
类型的订阅者。当我们使用 eventBus.post()
方法发布一个 MessageEvent
事件时,订阅者的 handleMessageEvent
方法将被调用,并输出收到的消息。
注意,如果订阅者处理事件的方法抛出异常,EventBus
默认情况下不会对异常进行处理。如果需要处理异常,可以在创建 EventBus
实例时传入一个自定义的 SubscriberExceptionHandler
五、进阶
观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子。
不同的应用场景和需求下,这个模式也有截然不同的实现方式,之前所列举的所有的例子都是同步阻塞的实现方式,当然我们的观察者设计模式也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。
之前讲到的实现方式,是一种同步阻塞的实现方式。观察者和被观察者代码在同一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成之后,才执行后续的代码。对照上面讲到的用户注册的例子,register() 函数依次调用执行每个观察者的 handleRegSuccess() 函数,等到都执行完成之后,才会返回结果给客户端。
如果注册接口是一个调用比较频繁的接口,对性能非常敏感,希望接口的响应时间尽可能短,那我们可以将同步阻塞的实现方式改为异步非阻塞的实现方式,以此来减少响应时间。
1) 异步非阻塞模型
首先,我们需要创建一个通用的观察者接口Observer
和一个被观察者接口Observable
。
Observer.java:
public interface Observer {
void update(String message);
}
Observable.java:
public interface Observable {
void addObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers(String message);
}
接下来,我们需要实现一个具体的被观察者类Subject
和一个具体的观察者类ConcreteObserver
。
Subject.java:
public class Subject implements Observable {
private List<Observer> observers;
private ExecutorService executorService;
public Subject() {
observers = new ArrayList<>();
executorService = Executors.newCachedThreadPool();
}
@Override
public void addObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String message) {
for (Observer observer : observers) {
executorService.submit(() -> observer.update(message));
}
}
public void setMessage(String message) {
notifyObservers(message);
}
}
ConcreteObserver.java:
public class ConcreteObserver implements Observer {
private String name;
public ConcreteObserver(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + " received message: " + message);
}
}
最后,我们可以创建一个简单的示例来测试实现的异步非阻塞观察者模式。
Main.java:
public class Main {
public static void main(String[] args) {
Subject subject = new Subject();
ConcreteObserver observer1 = new ConcreteObserver("Observer 1");
ConcreteObserver observer2 = new ConcreteObserver("Observer 2");
ConcreteObserver observer3 = new ConcreteObserver("Observer 3");
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.addObserver(observer3);
subject.setMessage("Hello, observers!");
// 等待异步任务完成
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在这个示例中,我们使用了ExecutorService
的线程池来实现异步非阻塞的通知。每个观察者更新操作都将作为一个任务提交给线程池并异步执行。这将确保性能敏感的场景不会因为观察者的通知而阻塞。