Java设计模式-观察者模式
Java设计模式-观察者模式
一、观察者模式初印象
嘿,各位 Java 大侠们!今天咱来唠唠设计模式里超有意思的观察者模式。先问大家一个问题,你们有没有追番的经历呢?想象一下,你是个超级动漫迷,最近迷上了一部超火的新番,比如《鬼灭之刃》那种让人热血沸腾的番剧 。
你每天都心心念念着它更新,要是手动每天去检查更新,那可太麻烦啦,万一忙起来还可能错过更新。但现在的视频网站可聪明啦,它们提供了 “追番” 功能。你只要轻轻一点 “追番” 按钮,就相当于告诉网站:“嘿,我盯上这部番啦,它一更新你可得马上通知我!” 网站就像个靠谱的小助手,把你的追番请求记下来。当番剧有新一集上线时,网站就会通过消息提醒、邮件或者 APP 推送等方式,欢快地告诉你:“您追的番更新啦,赶紧来观看!”
在这个追番场景里,其实就藏着观察者模式的影子。番剧本身就像是被观察的对象,我们叫它 “被观察者” 或者 “主题”;而你和其他追番的小伙伴们,就是观察者。当被观察者(番剧)状态发生变化(更新了),就会主动通知所有关注它的观察者(追番的用户)。这就是观察者模式最核心的思想:定义对象间一对多的依赖关系,当一个对象状态改变时,依赖它的多个对象能自动收到通知并更新 。是不是感觉和我们的生活场景很贴近呢?接下来,咱们就深入代码的世界,看看在 Java 里如何实现这个神奇的观察者模式 。
二、观察者模式的角色构成
观察者模式虽然看起来简单,但它有几个关键的角色,每个角色都有着独特的作用,它们相互协作,共同演绎出对象间的依赖与通知的精彩故事 。就像一场精彩的舞台剧,每个角色都不可或缺 。下面我们就来认识一下这些角色 。
(一)主题(Subject)
主题就像是这场舞台剧中的主角,它是被观察的对象 。在 Java 代码里,主题通常是一个接口或者抽象类 。它主要干两件大事:一是管理观察者列表,把那些关注它的观察者都记录下来,就好比一个明星有个粉丝团名单,上面写着所有粉丝的名字;二是当自己的状态发生改变时,负责通知列表里的观察者 。比如明星要开演唱会了(状态改变),就会通过各种渠道(通知)告诉粉丝们 。在 Java 中,主题接口可能会像下面这样定义:
public interface Subject {// 添加观察者void attach(Observer observer);// 删除观察者void detach(Observer observer);// 通知所有观察者void notifyObservers();
}
这个接口定义了管理观察者和通知观察者的基本方法,具体的主题类会实现这些方法,来完成实际的功能 。
(二)观察者(Observer)
观察者是关注主题状态变化的对象,它们就像是舞台下的观众,时刻盯着主角的一举一动 。在 Java 中,观察者通常也是一个接口或者抽象类,它只定义了一个关键的方法:更新方法(update) 。当主题的状态发生改变并通知观察者时,观察者就会调用这个更新方法来执行相应的操作 。比如观众看到明星有新动态(主题通知),就会做出反应(调用更新方法),可能是欢呼、尖叫,或者发微博表达激动的心情 。Java 中的观察者接口一般长这样:
public interface Observer {void update();
}
具体的观察者类会实现这个 update 方法,根据自身的需求来处理主题的通知 。
(三)具体主题(ConcreteSubject)
具体主题是主题接口的具体实现类,它就像是那个真实存在的明星,有着自己的实际状态和行为 。具体主题类除了要实现主题接口的方法,还要维护自己的状态 。当它的状态发生变化时,就会调用 notifyObservers 方法通知所有注册过的观察者 。比如某个明星真的确定了演唱会时间(状态变化),就会让经纪人(调用 notifyObservers 方法)通知粉丝 。下面是一个简单的具体主题类示例:
public class ConcreteSubject implements Subject {// 存储观察者的列表private List<Observer> observers = new ArrayList<>();// 主题的状态private String state;public String getState() {return state;}public void setState(String state) {this.state = state;// 状态改变时通知观察者notifyObservers();}@Overridepublic void attach(Observer observer) {observers.add(observer);}@Overridepublic void detach(Observer observer) {observers.remove(observer);}@Overridepublic void notifyObservers() {for (Observer observer : observers) {observer.update();}}
}
在这个类中,我们可以看到它通过一个 List 来存储观察者,并且在状态改变时,会遍历这个 List,依次调用每个观察者的 update 方法 。
(四)具体观察者(ConcreteObserver)
具体观察者是观察者接口的实现类,它代表了那些真实的观众 。每个具体观察者都持有一个对具体主题的引用,这样它就能知道是哪个主题在通知它 。当具体观察者接收到主题的通知时,会根据主题的状态来更新自己的状态 。比如某个粉丝收到了明星演唱会的通知(主题通知),就会赶紧查看自己的日程安排(更新自己的状态),看看能不能去参加 。下面是一个具体观察者类的示例:
public class ConcreteObserver implements Observer {// 持有具体主题的引用private ConcreteSubject subject;// 观察者自己的状态private String observerState;public ConcreteObserver(ConcreteSubject subject) {this.subject = subject;}@Overridepublic void update() {// 根据主题的状态更新自己的状态observerState = subject.getState();System.out.println("观察者状态更新为: " + observerState);}
}
在这个类中,我们可以看到它在构造函数中接收一个具体主题的实例,并且在 update 方法中,根据主题的状态来更新自己的状态 。
通过这四个角色的紧密配合,观察者模式就能够实现对象间的一对多依赖关系,当一个对象状态改变时,依赖它的多个对象能自动收到通知并更新 。就像明星和粉丝之间的互动,明星的一举一动都能牵动粉丝的心,而粉丝们也能及时做出回应 。
三、观察者模式底层原理剖析
(一)注册与注销机制
在观察者模式中,注册与注销机制就像是一场热闹的派对签到与签退。
我们先来说说注册机制。当观察者对主题感兴趣,想要关注主题的动态时,就会调用主题的attach
方法进行注册 。这个过程就好比你去参加一个超火的明星粉丝见面会,你在入口处签到,把自己的名字(观察者实例)登记在粉丝名单(主题维护的观察者列表)上 。主题会把这些观察者都妥善地保存在一个列表里,方便后续通知 。比如在前面的代码示例中,ConcreteSubject
类的attach
方法就是用来实现注册功能的:
@Override
public void attach(Observer observer) {observers.add(observer);
}
当你因为某些原因,比如突然有事要离开粉丝见面会,不想再关注这个明星的动态了(观察者不再想关注主题),就需要调用主题的detach
方法进行注销 。这就像你在粉丝见面会中途有事离开,在出口处签退,把自己的名字从粉丝名单上划掉 。主题会从观察者列表中移除对应的观察者 。还是以ConcreteSubject
类为例,它的detach
方法实现了注销功能:
@Override
public void detach(Observer observer) {observers.remove(observer);
}
这个注册与注销机制非常重要,它确保了主题能够准确地知道哪些观察者对它感兴趣,并且在状态变化时能够通知到正确的对象 。同时,也给了观察者很大的灵活性,它们可以随时决定是否要关注主题 。
(二)通知机制
当主题的状态发生变化时,就需要通知所有注册过的观察者,这就涉及到通知机制 。通知机制可以分为同步通知和异步通知 ,它们各有特点,就像不同的通知方式,有的快如闪电,有的则更灵活 。
1. 同步通知
同步通知就像是老师在教室里上课,老师(主题)讲完一个知识点(状态变化),马上就提问学生(通知观察者),学生们(观察者)必须立刻回答(执行更新操作) 。在代码中,同步通知通常是在主题的notifyObservers
方法中,直接遍历观察者列表,依次调用每个观察者的update
方法 。例如:
@Override
public void notifyObservers() {for (Observer observer : observers) {observer.update();}
}
这种方式的优点是简单直接,逻辑清晰,能够保证观察者及时收到通知并更新 。但缺点也很明显,如果观察者的更新操作比较耗时,就会阻塞主题的线程,导致主题在通知过程中无法处理其他任务 。比如老师提问后,有个学生回答问题特别慢,其他学生和老师都得等着,整个课堂进度就被拖慢了 。
2. 异步通知
异步通知则更像是你在网上购物,下单后(主题状态变化),商家会给你发个短信(通知),但你不一定会马上看到短信并处理(观察者不一定马上执行更新操作) 。在代码中,异步通知通常借助线程池等机制来实现 。当主题要通知观察者时,不是直接调用观察者的update
方法,而是把通知任务提交到线程池,由线程池中的线程来执行观察者的update
方法 。这样主题就不会被观察者的更新操作阻塞,可以继续处理其他任务 。比如下面的代码示例展示了如何使用线程池实现异步通知:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ConcreteSubject implements Subject {private List<Observer> observers = new ArrayList<>();private String state;// 创建线程池private ExecutorService executorService = Executors.newCachedThreadPool();public String getState() {return state;}public void setState(String state) {this.state = state;// 状态改变时异步通知观察者notifyObservers();}@Overridepublic void attach(Observer observer) {observers.add(observer);}@Overridepublic void detach(Observer observer) {observers.remove(observer);}@Overridepublic void notifyObservers() {for (Observer observer : observers) {executorService.submit(() -> observer.update());}}
}
异步通知的优点是提高了系统的响应性和并发性能,主题和观察者可以并行处理各自的任务 。但它也引入了一些复杂性,比如线程安全问题、任务调度和管理等 。就像你在网上购物收到短信通知后,可能会因为各种原因(比如手机没信号、静音等)错过通知,或者在处理通知时出现一些意外情况 。
(三)依赖关系维护
主题与观察者之间的一对多依赖关系维护是观察者模式的核心 。主题就像一个大明星,观察者则是它的粉丝,粉丝们依赖着明星的动态,而明星要管理好这些粉丝关系 。
主题通过维护一个观察者列表来建立和管理与观察者的依赖关系 。当有新的观察者注册时,主题把它添加到列表中;当观察者注销时,主题从列表中移除它 。这个列表就像是明星的粉丝团名单,记录着所有粉丝(观察者)的信息 。
而观察者则通过持有主题的引用,来保持与主题的联系 。当观察者接收到主题的通知时,它可以根据主题的状态来更新自己的状态 。比如在ConcreteObserver
类中,通过构造函数接收主题的引用:
public class ConcreteObserver implements Observer {// 持有具体主题的引用private ConcreteSubject subject;// 观察者自己的状态private String observerState;public ConcreteObserver(ConcreteSubject subject) {this.subject = subject;}@Overridepublic void update() {// 根据主题的状态更新自己的状态observerState = subject.getState();System.out.println("观察者状态更新为: " + observerState);}
}
这种依赖关系的维护方式,使得主题和观察者之间实现了松耦合 。主题不需要知道具体的观察者是谁,只需要按照接口规范通知观察者即可;观察者也不需要了解主题的内部实现细节,只关注主题的状态变化通知 。就像明星不需要知道每个粉丝的具体生活情况,只需要在有活动(状态变化)时通知粉丝;粉丝也不需要知道明星的日常工作细节,只关注明星的公开动态(通知) 。这种松耦合的设计,提高了系统的可维护性和可扩展性,方便我们在不影响其他部分的情况下,对主题或观察者进行修改和扩展 。
四、Java 代码实现观察者模式
(一)定义 Observer 接口
我们先定义一个Observer
接口,它就像是观察者们的 “任务清单”,规定了每个观察者必须要做的事情 —— 实现update
方法,用来接收主题传递过来的更新数据 。代码如下:
public interface Observer {// 接收更新数据的方法,这里以天气数据为例,传递温度、湿度、气压void update(float temperature, float humidity, float pressure);
}
在这个接口中,update
方法的参数就是主题传递给观察者的更新数据 。当主题状态改变时,会调用观察者的这个方法,把最新的数据传递过去 。就好比老师(主题)有了新的知识点(更新数据),就会叫学生(观察者)来接收 。
(二)定义 Subject 接口
接下来,我们定义Subject
接口,它是主题的 “行动指南”,定义了管理观察者和通知观察者的方法 。代码如下:
public interface Subject {// 添加观察者void registerObserver(Observer observer);// 删除观察者void removeObserver(Observer observer);// 通知所有观察者void notifyObservers();
}
registerObserver
方法用于将观察者添加到主题的观察者列表中,就像把新学生加入班级名单;removeObserver
方法则是将观察者从列表中移除,类似把学生从班级中除名;notifyObservers
方法就是主题在状态改变时,通知所有观察者的关键方法,如同老师宣布重要事情,让全班同学(所有观察者)都知道 。
(三)实现具体主题类
我们以一个天气数据中心为例,来实现具体主题类 。这个天气数据中心负责收集天气数据(温度、湿度、气压),并且在数据发生变化时,通知所有关注天气的观察者 。代码如下:
import java.util.ArrayList;
import java.util.List;public class WeatherData implements Subject {// 存储观察者的列表private List<Observer> observers = new ArrayList<>();// 温度private float temperature;// 湿度private float humidity;// 气压private float pressure;@Overridepublic void registerObserver(Observer observer) {observers.add(observer);}@Overridepublic void removeObserver(Observer observer) {if (observers.contains(observer)) {observers.remove(observer);}}@Overridepublic void notifyObservers() {for (Observer observer : observers) {observer.update(temperature, humidity, pressure);}}// 当天气数据有更新时调用这个方法public void setMeasurements(float temperature, float humidity, float pressure) {this.temperature = temperature;this.humidity = humidity;this.pressure = pressure;// 数据更新后通知所有观察者measurementsChanged();}// 通知观察者数据已改变private void measurementsChanged() {notifyObservers();}
}
在这个类中,observers
列表用来保存所有注册的观察者 。setMeasurements
方法用于更新天气数据,一旦数据更新,就会调用measurementsChanged
方法,进而调用notifyObservers
方法通知所有观察者 。在notifyObservers
方法中,会遍历observers
列表,依次调用每个观察者的update
方法,把最新的天气数据传递给它们 。
(四)实现具体观察者类
我们以一个天气展示面板为例,实现具体观察者类 。这个展示面板会实时显示天气数据,当接收到天气数据中心(主题)的通知时,它会更新自己显示的天气信息 。代码如下:
public class CurrentConditionsDisplay implements Observer {// 温度private float temperature;// 湿度private float humidity;// 持有主题的引用,方便获取数据private Subject weatherData;public CurrentConditionsDisplay(Subject weatherData) {this.weatherData = weatherData;// 注册自己为观察者weatherData.registerObserver(this);}@Overridepublic void update(float temperature, float humidity, float pressure) {this.temperature = temperature;this.humidity = humidity;// 显示最新的天气数据display();}// 显示天气数据的方法public void display() {System.out.println("Current conditions: " + temperature+ "F degrees and " + humidity + "% humidity");}
}
在这个类中,weatherData
引用指向主题(天气数据中心),通过构造函数传入 。在构造函数中,还会将自己注册到主题中 。当update
方法被调用时,会更新本地的温度和湿度数据,并调用display
方法显示最新的天气信息 。
(五)测试代码
最后,我们写一段测试代码来验证观察者模式是否正常工作 。在测试代码中,我们创建一个天气数据中心(主题)和一个天气展示面板(观察者),然后模拟天气数据的变化,观察展示面板是否能及时更新 。代码如下:
public class WeatherStation {public static void main(String[] args) {// 创建天气数据中心WeatherData weatherData = new WeatherData();// 创建天气展示面板,并将其注册到天气数据中心CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);// 模拟天气数据变化weatherData.setMeasurements(80, 65, 30.4f);weatherData.setMeasurements(82, 70, 29.2f);weatherData.setMeasurements(78, 90, 29.2f);}
}
运行这段测试代码,你会看到每当天气数据中心更新天气数据时,天气展示面板都会及时显示最新的天气信息 。这就说明我们实现的观察者模式成功地让主题和观察者之间建立了有效的通信和数据更新机制 。通过这个简单的示例,相信你已经对观察者模式在 Java 中的实现有了更深入的理解 。
五、观察者模式在 Java 中的应用场景
(一)事件监听机制
在 Java 的图形用户界面(GUI)编程中,比如使用 Swing 库时,事件监听机制就是观察者模式的典型应用 。就像你在玩一个超酷的桌面应用游戏,游戏里有很多按钮,比如 “开始游戏”“暂停游戏”“退出游戏” 等 。
这些按钮就是事件源,也就是我们观察者模式中的主题 。而你对这些按钮的操作,比如点击 “开始游戏” 按钮,就会触发一个点击事件 。为了处理这个事件,我们需要注册一个监听器,这个监听器就是观察者 。当按钮被点击(主题状态改变)时,就会通知注册的监听器(观察者),监听器会执行相应的操作,比如开始加载游戏资源,显示游戏界面等 。
下面我们来看一段简单的 Swing 代码示例,展示按钮点击事件中的观察者模式应用 :
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;public class ButtonClickListenerExample {public static void main(String[] args) {// 创建一个按钮,它是事件源(主题)JButton button = new JButton("点击我");// 创建一个JFrame来容纳按钮JFrame frame = new JFrame("按钮点击示例");frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.setSize(300, 200);frame.add(button);frame.setVisible(true);// 注册一个监听器(观察者)button.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {// 当按钮被点击时,执行这里的操作System.out.println("按钮被点击啦!");}});}
}
在这段代码中,JButton
类相当于具体主题,它维护了一个监听器列表 。addActionListener
方法用于注册监听器,把监听器添加到列表中 。当按钮被点击时,就会调用监听器的actionPerformed
方法,这就相当于主题通知观察者,观察者执行相应的更新操作 。通过这种方式,我们实现了用户操作与程序逻辑的解耦,使得代码更加灵活和可维护 。就像游戏开发者不需要关心玩家什么时候点击按钮,只需要定义好点击按钮后的操作即可;玩家也不需要知道游戏内部是如何处理按钮点击事件的,只需要点击按钮就能触发相应的功能 。
(二)消息推送系统
在消息推送系统中,观察者模式也有着广泛的应用 。想象一下,你是一个社交软件的用户,比如使用微信 。微信服务器就像是主题,而你和其他众多用户就是观察者 。
当有新消息产生时,比如你的好友给你发了一条消息,微信服务器(主题)会将这条消息推送给所有订阅了消息的客户端(观察者) 。微信服务器会维护一个用户列表,记录着所有订阅消息的用户 。当有新消息时,它会遍历这个列表,把消息发送给每个用户 。
在 Java 代码实现中,我们可以用一个简单的示例来模拟消息推送系统 :
import java.util.ArrayList;
import java.util.List;// 观察者接口
interface MessageObserver {void receiveMessage(String message);
}// 具体主题类,代表消息服务器
class MessageServer {private List<MessageObserver> observers = new ArrayList<>();// 添加观察者public void addObserver(MessageObserver observer) {observers.add(observer);}// 删除观察者public void removeObserver(MessageObserver observer) {observers.remove(observer);}// 推送消息,通知所有观察者public void sendMessage(String message) {for (MessageObserver observer : observers) {observer.receiveMessage(message);}}
}// 具体观察者类,代表客户端
class MessageClient implements MessageObserver {private String clientName;public MessageClient(String clientName) {this.clientName = clientName;}@Overridepublic void receiveMessage(String message) {System.out.println(clientName + "收到消息: " + message);}
}
测试代码:
public class MessagePushSystemTest {public static void main(String[] args) {// 创建消息服务器MessageServer server = new MessageServer();// 创建两个客户端MessageClient client1 = new MessageClient("用户A");MessageClient client2 = new MessageClient("用户B");// 将客户端注册到服务器server.addObserver(client1);server.addObserver(client2);// 服务器推送消息server.sendMessage("这是一条重要通知!");// 移除一个客户端server.removeObserver(client1);// 再次推送消息server.sendMessage("又有新消息啦!");}
}
在这个示例中,MessageServer
类负责管理观察者列表,并在有新消息时通知所有观察者 。MessageClient
类实现了MessageObserver
接口,当收到消息时会打印出消息内容 。通过这种方式,我们实现了消息的推送功能,就像微信服务器能及时把消息推送给用户一样 。
(三)分布式系统中的数据同步
在分布式系统中,数据往往分布在多个节点上 。为了保证数据的一致性,当一个节点的数据发生变化时,需要通知其他节点进行更新,这就可以借助观察者模式来实现 。
比如,有一个分布式电商系统,商品库存数据存储在多个服务器节点上 。当某个节点上的库存数据发生变化时,比如某个商品卖出了一件,库存减少,这个节点(主题)需要通知其他存储该商品库存数据的节点(观察者),让它们也更新自己的库存数据 。
在 Java 中,可以使用一些分布式框架来实现这种数据同步的观察者模式 。以 Zookeeper 为例,Zookeeper 是一个分布式协调服务,它提供了一种监听机制(Watcher) 。当 Zookeeper 中某个节点的数据发生变化时,可以触发相应的事件通知 。下面是一个简单的 Java 示例代码,演示如何使用 Zookeeper 实现数据同步的通知:
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;import java.io.IOException;
import java.util.concurrent.CountDownLatch;public class ZookeeperDataSyncExample {private static final String ZK_SERVERS = "localhost:2181";private static final int SESSION_TIMEOUT = 5000;private ZooKeeper zk;private CountDownLatch connectedSignal = new CountDownLatch(1);public void connect() throws IOException, InterruptedException {zk = new ZooKeeper(ZK_SERVERS, SESSION_TIMEOUT, new Watcher() {@Overridepublic void process(WatchedEvent event) {if (event.getState() == Event.KeeperState.SyncConnected) {connectedSignal.countDown();}}});connectedSignal.await();}public void close() throws InterruptedException {zk.close();}public void createNode(String path, byte[] data) throws KeeperException, InterruptedException {zk.create(path, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);}public void setData(String path, byte[] data) throws KeeperException, InterruptedException {zk.setData(path, data, -1);// 数据更新后,会触发Watcher通知其他节点}public byte[] getData(String path, boolean watch) throws KeeperException, InterruptedException {return zk.getData(path, watch, null);}public static void main(String[] args) throws Exception {ZookeeperDataSyncExample client = new ZookeeperDataSyncExample();client.connect();String path = "/product/stock";byte[] initialData = "100".getBytes();// 初始库存100byte[] updatedData = "99".getBytes();// 卖出一件后,库存更新为99// 创建节点并设置初始数据client.createNode(path, initialData);System.out.println("节点创建并设置初始库存: " + new String(initialData));// 获取数据并设置Watcherbyte[] data = client.getData(path, true);System.out.println("初始库存: " + new String(data));// 更新节点数据client.setData(path, updatedData);System.out.println("库存更新为: " + new String(updatedData));// 等待一段时间以观察Watcher是否被触发,其他节点是否收到通知更新数据Thread.sleep(5000);client.close();}
}
在这个示例中,当调用setData
方法更新 Zookeeper 节点的数据时,会触发注册的 Watcher,通知其他监听该节点的客户端(观察者),它们可以通过重新获取数据来实现数据同步 。通过这种方式,在分布式系统中实现了数据的一致性维护,确保各个节点的数据能够及时更新 。
六、观察者模式的优缺点大揭秘
(一)优点
-
松耦合:观察者模式就像是一个超级社交达人,它让主题和观察者之间的关系变得非常松散 。主题不需要知道具体有哪些观察者,也不用关心观察者会如何处理通知,它只需要按照约定的接口去通知观察者就行了 。观察者也不需要了解主题的内部实现细节,只关注主题的通知信息 。就好比你在一个大型社交平台上关注了很多博主(主题),博主们(主题)不用知道你(观察者)是谁,也不用知道你会怎么使用他们发布的内容(通知),只要把内容发布出来(通知)就行;你也不用知道博主们是怎么制作内容的,只需要接收内容并根据自己的喜好去处理,比如点赞、评论或者收藏 。这种松耦合的设计使得系统的可维护性和可扩展性大大提高,当我们需要增加新的观察者或者修改主题的实现时,不会对其他部分造成太大的影响 。
-
可扩展性:观察者模式具有很强的可扩展性,就像一个可以无限拼接的积木 。当我们的系统有新的需求,需要添加新的观察者来处理主题的状态变化时,只需要创建一个新的观察者类并实现观察者接口,然后将其注册到主题中即可,不需要对主题的代码进行大规模修改 。例如在一个电商系统中,原来只有订单创建时通知财务部门更新账目(一个观察者),后来业务拓展,需要在订单创建时通知物流部门准备发货(新增一个观察者),我们只需要创建一个物流部门的观察者类,实现相应的更新方法,然后在订单(主题)中注册这个观察者,就可以轻松实现新的功能 。这种可扩展性使得系统能够灵活地适应不断变化的业务需求 。
-
实时更新:观察者模式能够实现实时更新,就像一个实时播报的新闻台 。当主题的状态发生变化时,会立即通知所有注册的观察者,观察者可以及时做出响应并更新自己的状态 。比如在一个股票交易系统中,股票价格(主题)实时变化,投资者(观察者)的股票界面会实时显示最新的价格,投资者可以根据最新价格及时做出交易决策 。这种实时更新的特性在很多对实时性要求较高的系统中非常重要,能够保证系统中各个部分的数据一致性和及时性 。
(二)缺点
-
内存占用:如果有大量的观察者注册到主题中,会占用较多的内存 。因为主题需要维护一个观察者列表,每个观察者也可能持有一些资源 。就好比一个超级热门的明星,有无数粉丝(观察者)关注他,明星(主题)需要维护一个庞大的粉丝名单,这个名单会占用大量的存储空间;而且每个粉丝也有自己的生活和资源,这就导致整个系统的内存占用增加 。当观察者数量过多时,可能会影响系统的性能,甚至导致内存溢出等问题 。所以在使用观察者模式时,需要注意控制观察者的数量,或者采用一些优化策略,比如在观察者不再使用时及时将其从主题中注销 。
-
通知顺序不确定:在观察者模式中,当主题通知观察者时,通知的顺序是不确定的 。因为主题通常是通过遍历观察者列表来通知观察者的,而列表的遍历顺序可能会受到多种因素的影响 。这就好比一群人在排队领东西,但是没有规定严格的排队顺序,那么每个人领到东西的顺序就不确定 。在某些情况下,这种不确定的通知顺序可能会导致问题 。例如在一个游戏中,有多个游戏元素(观察者)依赖于游戏角色(主题)的状态变化,有些元素需要先更新,有些需要后更新,如果通知顺序不确定,可能会导致游戏逻辑出现错误 。为了解决这个问题,我们可能需要对观察者进行分类或者编号,在通知时按照特定的顺序进行通知 。
-
可能导致循环依赖:如果不小心设计,观察者和主题之间可能会形成循环依赖 。比如观察者 A 依赖主题 B 的状态变化,而主题 B 又依赖观察者 A 的某些操作结果来改变自己的状态,这样就形成了一个死循环 。就好比两个人互相盯着对方,等对方先动,结果谁也动不了 。循环依赖会导致系统陷入无限循环,最终可能导致系统崩溃 。为了避免循环依赖,在设计时需要仔细分析观察者和主题之间的依赖关系,确保依赖关系是单向的,或者采用一些打破循环的策略,比如引入中间层来解耦 。
-
通知开销:当主题的状态发生变化时,需要通知所有注册的观察者,这个通知过程可能会带来一定的开销 。如果观察者的更新操作比较耗时,那么通知所有观察者的时间就会很长,影响系统的性能 。特别是在观察者数量较多的情况下,这种开销可能会更加明显 。就好比一个老师要通知全班同学一件事情,每个同学听到通知后都要做一些复杂的事情,那么这个通知过程就会花费很长时间,导致整个班级的效率降低 。为了减少通知开销,可以采用异步通知的方式,将通知任务放到线程池中执行,避免阻塞主题的线程;或者对观察者进行分组,根据不同的优先级或者业务场景进行分批通知 。
七、实际项目中如何优化观察者模式
(一)减少内存占用
在实际项目中,当有大量观察者注册到主题时,内存占用可能会成为一个严重的问题 。为了减少内存占用,我们可以采取以下几种方法 。
- 使用弱引用:在主题中,我们可以使用弱引用来存储观察者 。弱引用不会阻止观察者对象被垃圾回收器回收,当观察者对象没有其他强引用指向它时,垃圾回收器会在合适的时候回收它 。在 Java 中,我们可以使用
WeakHashMap
来实现这一点 。WeakHashMap
的键是弱引用,当键对象不再被其他地方引用时,它会被自动从WeakHashMap
中移除 。例如:
import java.util.WeakHashMap;public class Subject {// 使用WeakHashMap存储观察者private WeakHashMap<Observer, Object> observers = new WeakHashMap<>();public void attach(Observer observer) {observers.put(observer, null);}public void detach(Observer observer) {observers.remove(observer);}public void notifyObservers() {// 这里需要注意,在遍历WeakHashMap时,可能会有元素被回收,所以要做好处理for (Observer observer : observers.keySet()) {if (observer != null) {observer.update();}}}
}
这样,当观察者不再被使用且没有其他强引用时,它会被垃圾回收,从而减少内存占用 。
2. 及时注销观察者:在观察者不再需要接收主题通知时,要及时调用主题的detach
方法将其注销 。比如在一个图形界面应用中,当某个窗口关闭(窗口是观察者),它不再需要接收某些系统事件(主题通知),就应该及时从事件源(主题)中注销自己 。这可以避免无效的观察者占用内存 。
3. 限制观察者数量:在设计系统时,要根据实际业务需求合理限制观察者的数量 。如果某些观察者只是在特定时间段内需要接收通知,可以在这段时间结束后及时将其注销 。比如在一个电商促销活动中,某些临时的促销规则观察者,在促销活动结束后就可以被注销,不再占用内存 。
(二)优化通知性能
当主题需要通知大量观察者时,通知性能可能会受到影响 。以下是一些优化通知性能的策略 。
- 批量通知:如果观察者的更新操作可以批量处理,我们可以将多个观察者的更新操作合并成一次处理 。比如在一个数据更新的场景中,多个视图(观察者)都需要根据数据的更新来刷新显示 。我们可以先收集所有需要更新的视图,然后一次性通知它们进行更新,而不是逐个通知 。可以使用一个临时的集合来存储需要通知的观察者,然后统一进行处理 。
public class Subject {private List<Observer> observers = new ArrayList<>();private List<Observer> tempObservers = new ArrayList<>();public void attach(Observer observer) {observers.add(observer);}public void detach(Observer observer) {observers.remove(observer);}public void notifyObservers() {tempObservers.addAll(observers);// 这里可以进行一些批量处理的逻辑,比如批量更新数据库等for (Observer observer : tempObservers) {observer.update();}tempObservers.clear();}
}
- 异步通知:采用异步通知的方式可以避免主题线程被观察者的更新操作阻塞 。我们可以使用线程池来实现异步通知 。当主题状态改变时,将通知任务提交到线程池,由线程池中的线程来执行观察者的更新方法 。这样主题可以继续处理其他任务,提高系统的响应性 。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Subject {private List<Observer> observers = new ArrayList<>();// 创建线程池private ExecutorService executorService = Executors.newCachedThreadPool();public void attach(Observer observer) {observers.add(observer);}public void detach(Observer observer) {observers.remove(observer);}public void notifyObservers() {for (Observer observer : observers) {executorService.submit(() -> observer.update());}}
}
- 过滤通知:在通知观察者之前,根据业务逻辑对观察者进行过滤,只通知那些真正需要接收通知的观察者 。比如在一个权限管理系统中,某些操作的通知只需要发送给具有特定权限的用户(观察者) 。可以在
notifyObservers
方法中添加过滤逻辑 。
public class Subject {private List<Observer> observers = new ArrayList<>();public void attach(Observer observer) {observers.add(observer);}public void detach(Observer observer) {observers.remove(observer);}public void notifyObservers() {for (Observer observer : observers) {if (shouldNotify(observer)) {observer.update();}}}private boolean shouldNotify(Observer observer) {// 这里添加具体的过滤逻辑,比如根据权限判断等return true;}
}
(三)确保线程安全
在多线程环境下使用观察者模式时,需要确保线程安全,以避免数据不一致或其他并发问题 。
- 使用线程安全集合:在主题中存储观察者时,使用线程安全的集合 。比如使用
CopyOnWriteArrayList
代替ArrayList
。CopyOnWriteArrayList
在进行写操作(如添加、删除元素)时,会创建一个新的数组,读操作则在旧数组上进行,这样可以保证读操作的线程安全,并且读操作不会被写操作阻塞 。
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;public class Subject {// 使用CopyOnWriteArrayList存储观察者private List<Observer> observers = new CopyOnWriteArrayList<>();public void attach(Observer observer) {observers.add(observer);}public void detach(Observer observer) {observers.remove(observer);}public void notifyObservers() {for (Observer observer : observers) {observer.update();}}
}
- 同步机制:在主题的关键方法(如
attach
、detach
、notifyObservers
)上使用synchronized
关键字进行同步 。这可以保证在同一时刻只有一个线程能够执行这些方法,避免多线程并发访问导致的数据不一致问题 。
public class Subject {private List<Observer> observers = new ArrayList<>();public synchronized void attach(Observer observer) {observers.add(observer);}public synchronized void detach(Observer observer) {observers.remove(observer);}public synchronized void notifyObservers() {for (Observer observer : observers) {observer.update();}}
}
- 使用并发工具类:Java 提供了一些并发工具类,如
CountDownLatch
、CyclicBarrier
等,可以帮助我们更好地处理多线程环境下的观察者模式 。比如使用CountDownLatch
可以确保所有观察者都完成更新操作后,主题再继续执行后续任务 。假设主题在通知观察者后,需要等待所有观察者更新完成,才能进行下一步操作 。
import java.util.List;
import java.util.concurrent.CountDownLatch;public class Subject {private List<Observer> observers = new ArrayList<>();public void attach(Observer observer) {observers.add(observer);}public void detach(Observer observer) {observers.remove(observer);}public void notifyObservers() {CountDownLatch latch = new CountDownLatch(observers.size());for (Observer observer : observers) {new Thread(() -> {observer.update();latch.countDown();}).start();}try {latch.await();// 所有观察者更新完成,执行后续操作} catch (InterruptedException e) {e.printStackTrace();}}
}
八、总结
好啦,各位小伙伴们!到这里,我们对观察者模式的探索之旅就暂告一段落啦 。回顾一下,观察者模式就像一个超级社交网络,定义了对象间一对多的依赖关系,当一个对象(主题)状态改变时,依赖它的多个对象(观察者)能自动收到通知并更新 。它有主题、观察者、具体主题和具体观察者这几个关键角色,每个角色都有着独特的作用,共同演绎出对象间的依赖与通知的精彩故事 。
在底层原理上,注册与注销机制就像是派对的签到与签退,让主题能准确管理观察者;通知机制分为同步和异步,同步通知像老师课堂提问,异步通知则像网购时商家的短信通知,各有特点;依赖关系维护使得主题和观察者之间实现了松耦合,就像明星和粉丝,互不干扰又紧密相连 。
在 Java 代码实现中,我们通过定义接口和类,实现了主题与观察者之间的通信和数据更新 。而且观察者模式在 Java 的事件监听机制、消息推送系统、分布式系统中的数据同步等场景都有着广泛的应用 。当然,它也有优点和缺点,优点如松耦合、可扩展性强、实时更新等,缺点像内存占用、通知顺序不确定等 。不过别担心,我们还学习了在实际项目中通过减少内存占用、优化通知性能、确保线程安全等策略来优化观察者模式 。
希望大家通过这篇文章,对观察者模式有了更深入的理解 。在今后的 Java 开发项目中,当遇到需要实现对象间一对多依赖关系和通知的场景时,不妨试试观察者模式,让它为你的代码增添一份优雅和灵活 。设计模式的世界丰富多彩,观察者模式只是其中的一小部分,还有更多有趣又强大的设计模式等待着你去探索 。加油,各位 Java 大侠们,让我们在代码的世界里继续乘风破浪!