通俗易懂的故事解说观察者模式
想象一下,你是一位报社的老板,你的报社每天都会出版最新的报纸。为了让报纸能够卖出去,你需要让更多的人知道报纸的更新。于是,你想出了一个办法:让读者订阅你的报纸。
- 读者订阅:读者来到报社,填写订阅表格,留下自己的联系方式(比如邮箱地址)。这样,你就知道哪些读者想要收到你的报纸。
- 报纸出版:每当你的报社出版了新的报纸,你不需要挨家挨户去送报纸,而是只需要给所有订阅了报纸的读者群发一封邮件,邮件里附带报纸的电子版或者告知他们报纸已经出版,可以前来购买。
- 读者阅读:订阅了报纸的读者收到邮件后,就可以下载电子版报纸阅读,或者直接去报摊购买纸质版报纸。
在这个故事中:
- 报社就相当于被观察者(Subject) ,也叫做主题。它负责维护订阅者列表,并在状态发生变化时通知订阅者。
- 读者就相当于观察者(Observer) 。他们订阅了报社(被观察者),并等待接收报社的通知。
- 订阅报纸的行为就相当于注册观察者。读者通过订阅,将自己的联系方式注册到报社的观察者列表中。
- 报纸出版并通知读者的行为就相当于被观察者状态改变,并通知所有观察者。
- 读者收到邮件并阅读报纸就相当于观察者接收到通知,并执行相应的操作(比如下载报纸、购买报纸)。
观察者模式就像报社和订阅者之间的关系,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象(被观察者)。当主题对象的状态发生变化时,它会通知所有的观察者对象,使它们能够自动更新自己的状态。
Java实现几种常用的观察者模式
下面,我用Java代码来实现几种常用的观察者模式,并讲解它们的优缺点。
1. 手动实现观察者模式(推模型 - Push Model)
场景:报社(被观察者)在报纸出版时,不仅通知订阅者(观察者)报纸出版了,还主动推送报纸的标题和摘要给订阅者。
java
import java.util.ArrayList;
import java.util.List;
// 1. 定义观察者接口
interface Observer {
void update(String title, String summary);
}
// 2. 定义被观察者接口
interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers(String title, String summary);
}
// 3. 实现被观察者(报社)
class NewspaperPublisher implements Subject {
private List<Observer> observers;
private String title;
private String summary;
public NewspaperPublisher() {
observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String title, String summary) {
this.title = title;
this.summary = summary;
for (Observer observer : observers) {
observer.update(title, summary); // 推送标题和摘要给观察者
}
}
// 报社出版报纸的方法
public void publishNewspaper(String title, String summary) {
System.out.println("报社正在出版报纸: " + title);
notifyObservers(title, summary); // 通知所有订阅者
}
}
// 4. 实现观察者(读者)
class Reader implements Observer {
private String name;
public Reader(String name) {
this.name = name;
}
@Override
public void update(String title, String summary) {
System.out.println(name + " 收到报纸更新通知: 标题 - "" + title + "", 摘要 - "" + summary + """);
// 读者可以根据标题和摘要决定是否下载或购买报纸
}
}
// 5. 客户端代码
public class ObserverPatternDemo {
public static void main(String[] args) {
NewspaperPublisher publisher = new NewspaperPublisher();
Observer reader1 = new Reader("读者A");
Observer reader2 = new Reader("读者B");
publisher.registerObserver(reader1);
publisher.registerObserver(reader2);
publisher.publishNewspaper("今日要闻", "这是今天的头条新闻摘要...");
publisher.removeObserver(reader1);
publisher.publishNewspaper("明日预告", "这是明天的报纸内容预告...");
}
}
优点:
- 解耦:被观察者和观察者之间完全解耦,它们之间只通过观察者接口进行通信。被观察者不需要知道观察者的具体实现细节,观察者也不需要知道被观察者的内部状态。
- 灵活:可以动态地注册和移除观察者,实现灵活的通知机制。
- 可扩展:可以方便地添加新的观察者,而无需修改被观察者的代码,符合开闭原则。
- 推模型:被观察者可以主动推送数据给观察者,观察者可以直接使用这些数据,而不需要自己再去获取。
缺点:
- 推模型:如果被观察者推送的数据量很大,或者观察者不需要所有数据,可能会造成网络带宽和资源的浪费。
- 性能:如果观察者数量很多,通知所有观察者可能会影响性能。
2. 手动实现观察者模式(拉模型 - Pull Model)
场景:报社(被观察者)在报纸出版时,只通知订阅者(观察者)报纸出版了,但不主动推送报纸内容。订阅者收到通知后,主动去报社获取报纸的标题和摘要。
java
import java.util.ArrayList;
import java.util.List;
// 1. 定义观察者接口(与推模型相同)
interface Observer {
void update(); // 不再接收推送的数据
}
// 2. 定义被观察者接口(与推模型相同)
interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers(); // 不再推送数据
}
// 3. 实现被观察者(报社)
class NewspaperPublisher implements Subject {
private List<Observer> observers;
private String title;
private String summary;
public NewspaperPublisher() {
observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(); // 仅通知观察者,不推送数据
}
}
// 报社出版报纸的方法
public void publishNewspaper(String title, String summary) {
System.out.println("报社正在出版报纸: " + title);
this.title = title;
this.summary = summary;
notifyObservers(); // 通知所有订阅者
}
// 提供给观察者获取报纸信息的方法
public String getTitle() {
return title;
}
public String getSummary() {
return summary;
}
}
// 4. 实现观察者(读者)
class Reader implements Observer {
private String name;
private NewspaperPublisher publisher;
public Reader(String name, NewspaperPublisher publisher) {
this.name = name;
this.publisher = publisher;
}
@Override
public void update() {
// 主动去报社获取报纸信息
String title = publisher.getTitle();
String summary = publisher.getSummary();
System.out.println(name + " 收到报纸更新通知,并主动获取报纸: 标题 - "" + title + "", 摘要 - "" + summary + """);
// 读者可以根据标题和摘要决定是否下载或购买报纸
}
}
// 5. 客户端代码(与推模型类似,只是创建Reader时需要传入NewspaperPublisher实例)
public class ObserverPatternDemo {
public static void main(String[] args) {
NewspaperPublisher publisher = new NewspaperPublisher();
Observer reader1 = new Reader("读者A", publisher);
Observer reader2 = new Reader("读者B", publisher);
publisher.registerObserver(reader1);
publisher.registerObserver(reader2);
publisher.publishNewspaper("今日要闻", "这是今天的头条新闻摘要...");
publisher.removeObserver(reader1);
publisher.publishNewspaper("明日预告", "这是明天的报纸内容预告...");
}
}
优点:
- 解耦、灵活、可扩展:与推模型相同。
- 拉模型:观察者可以根据需要主动获取数据,避免了推模型中可能存在的数据浪费问题。
- 被观察者无需知道观察者需要哪些数据:被观察者只需要通知观察者状态发生了变化,而不需要关心观察者需要哪些具体的数据。
缺点:
- 拉模型:观察者需要主动去获取数据,可能增加观察者的复杂度。
- 性能:如果观察者获取数据的操作比较耗时,可能会影响性能。
- 数据一致性:如果被观察者的状态在观察者获取数据的过程中发生了变化,观察者获取到的数据可能不是最新的。
3. 使用Java内置的观察者模式(已过时,仅作了解)
Java曾经在java.util包中提供了Observable类和Observer接口,用于实现观察者模式。但是,从Java 9开始,这些类和接口被标记为过时(Deprecated),不推荐在新代码中使用。这里仅作了解:
java
import java.util.Observable;
import java.util.Observer;
// 1. 被观察者(继承自Observable)
class NewspaperPublisher extends Observable {
private String title;
private String summary;
public void publishNewspaper(String title, String summary) {
System.out.println("报社正在出版报纸: " + title);
this.title = title;
this.summary = summary;
setChanged(); // 标记状态已改变
notifyObservers(); // 通知所有观察者(拉模型)
// notifyObservers(new Object()); // 通知所有观察者(推模型,可以推送一个对象)
}
// 提供给观察者获取报纸信息的方法(拉模型)
public String getTitle() {
return title;
}
public String getSummary() {
return summary;
}
}
// 2. 观察者(实现Observer接口)
class Reader implements Observer {
private String name;
public Reader(String name) {
this.name = name;
}
@Override
public void update(Observable o, Object arg) {
NewspaperPublisher publisher = (NewspaperPublisher) o;
// 拉模型:主动获取数据
String title = publisher.getTitle();
String summary = publisher.getSummary();
System.out.println(name + " 收到报纸更新通知,并主动获取报纸: 标题 - "" + title + "", 摘要 - "" + summary + """);
// 推模型:如果使用 notifyObservers(new Object()),可以通过 arg 参数获取推送的数据
// if (arg != null) {
// // 处理推送的数据
// }
}
}
// 3. 客户端代码
public class ObserverPatternDemo {
public static void main(String[] args) {
NewspaperPublisher publisher = new NewspaperPublisher();
Observer reader1 = new Reader("读者A");
Observer reader2 = new Reader("读者B");
publisher.addObserver(reader1);
publisher.addObserver(reader2);
publisher.publishNewspaper("今日要闻", "这是今天的头条新闻摘要...");
publisher.deleteObserver(reader1);
publisher.publishNewspaper("明日预告", "这是明天的报纸内容预告...");
}
}
优点:
- 简单易用:Java内置了观察者模式的实现,使用起来非常方便。
缺点:
- 已过时:从Java 9开始,
Observable和Observer被标记为过时,不推荐在新代码中使用。 - 功能有限:Java内置的观察者模式功能相对简单,可能无法满足复杂场景的需求。
- 推模型和拉模型混合:Java内置的观察者模式同时支持推模型和拉模型,但使用起来可能不够清晰。
总结
| 观察者模式实现方式 | 推模型/拉模型 | 线程安全 | 实现难度 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 手动实现(推模型) | 推模型 | 否 | 简单 | 解耦、灵活、可扩展、推模型(主动推送数据) | 推模型(可能造成数据浪费)、性能(观察者数量多时) |
| 手动实现(拉模型) | 拉模型 | 否 | 简单 | 解耦、灵活、可扩展、拉模型(观察者主动获取数据,避免数据浪费) | 拉模型(观察者需要主动获取数据,可能增加复杂度)、性能、数据一致性 |
| Java内置(已过时) | 推/拉混合 | 否 | 简单 | 简单易用 | 已过时、功能有限、推模型和拉模型混合 |
在实际开发中,可以根据具体的需求和场景,选择最合适的观察者模式实现方式。如果对线程安全有较高要求,或者需要更强大的功能,可以考虑使用第三方库(如Guava的EventBus)或框架(如Spring的事件机制)来实现观察者模式。如果只是需要简单的观察者模式,手动实现也是一个不错的选择。