解锁设计模式:一文吃透观察者模式
观察者模式通过定义对象间一对多的依赖关系,让一个对象状态变化时自动通知并更新依赖它的对象,实现了对象间的解耦 。它由主题、具体主题、观察者和具体观察者组成,工作流程包括注册观察者、状态改变、通知观察者以及观察者更新这几个关键步骤。在实际应用中,观察者模式优点明显,它实现了对象之间的松耦合,支持动态交互,还能满足多种订阅方式的需求,因此在事件处理系统、数据绑定与 MVVM 模式、消息队列与发布 -
目录
一、什么是观察者模式
观察者模式(Observer Pattern),是一种行为型设计模式,也被称为发布 - 订阅模式(Publish-Subscribe Pattern) 。这种模式定义了对象之间的一种一对多依赖关系,使得每当一个对象(被观察者)的状态发生改变时,其所有依赖于它的对象(观察者)都会得到通知并被自动更新。
打个比方,就拿我们日常使用的公众号来说。当你关注了一个公众号,这个公众号就相当于被观察者,而你以及其他关注这个公众号的粉丝们就是观察者。一旦公众号发布了新文章(也就是状态发生改变),平台就会自动通知到所有关注它的粉丝(所有观察者收到通知并更新,比如在各自的微信端收到推送消息)。再比如,在股票交易系统中,每只股票就是被观察者,而股民们就是观察者。当某只股票的价格发生变化时,关注这只股票的股民就会收到通知,进而可以根据价格变化做出相应的决策。
二、观察者模式的组成
观察者模式主要包含四个核心角色:主题(Subject)、具体主题(Concrete Subject)、观察者(Observer)和具体观察者(Concrete Observer)。下面我将为大家一一介绍。
2.1 主题(Subject)
主题也被称为抽象目标类,它是一个接口或者抽象类 ,定义了一系列管理观察者的方法,是整个观察者模式的核心纽带。其主要作用是提供一个统一的管理观察者的接口,使得具体主题类可以方便地添加、删除和通知观察者。在主题接口或抽象类中,通常会包含以下方法:
-
注册观察者方法:用于将观察者对象添加到主题的观察者列表中,使主题能够知道有哪些观察者对它的状态变化感兴趣。比如在前面提到的公众号例子中,公众号提供的 “关注” 功能就相当于注册观察者的方法,用户通过关注公众号,将自己注册为观察者。
-
注销观察者方法:与注册方法相反,用于将不再感兴趣的观察者从观察者列表中移除。例如公众号的 “取消关注” 功能,用户取消关注公众号,就相当于从公众号的观察者列表中注销自己。
-
通知观察者方法:当主题的状态发生变化时,调用此方法通知所有注册的观察者,告知它们主题状态已改变,以便观察者做出相应的处理。如公众号发布新文章后,会通过通知方法向所有关注它的用户发送推送通知。
2.2 具体主题(Concrete Subject)
具体主题类是主题接口的具体实现,它负责维护一个观察者列表,存储所有对其感兴趣的观察者对象 。当具体主题的内部状态发生改变时,它会遍历观察者列表,调用每个观察者的更新方法,通知它们状态已改变。
以股票交易系统为例,某只股票就是一个具体主题。它会维护一个包含所有关注这只股票股民(观察者)的列表。当股票价格发生变化时(状态改变),股票这个具体主题就会通知列表中的所有股民,告知他们股票价格的变动情况,股民们(观察者)则可以根据这个通知做出买卖股票的决策。在具体实现中,具体主题类通常会在其内部定义一个用于存储观察者对象的集合,比如 Java 中的ArrayList,然后实现主题接口中定义的注册、注销和通知观察者的方法。
2.3 观察者(Observer)
观察者是一个接口或者抽象类,它为所有具体观察者定义了一个统一的更新接口 。这个接口的主要作用是让具体观察者实现一个方法,当它们接收到主题的通知时,能够执行相应的操作来更新自身的状态,以与主题的状态保持同步。在观察者接口或抽象类中,一般只定义一个核心的更新方法,这个方法通常会接收主题传递过来的一些状态信息,以便观察者根据这些信息进行自身状态的更新。
例如在一个游戏场景中,玩家角色是观察者,游戏中的各种事件(如怪物出现、道具刷新等)是主题。玩家角色实现了观察者接口,当怪物出现这个主题发生状态变化(怪物出现了)时,玩家角色作为观察者,会通过实现的更新方法来做出反应,比如准备战斗、调整策略等。
2.4 具体观察者(Concrete Observer)
具体观察者类实现了观察者接口,它是真正对主题状态变化感兴趣并做出具体响应的对象 。每个具体观察者都有自己独特的业务逻辑,当接收到主题的通知时,它们会根据自身的需求和逻辑,利用主题传递过来的信息来更新自己的状态或者执行特定的操作,从而与主题的状态保持一致。
继续以上述游戏场景为例,游戏中的不同玩家就是具体观察者。当怪物出现的通知传来时,战士玩家可能会选择直接冲上去攻击怪物;法师玩家则可能会开始吟唱法术,准备远程攻击;而辅助玩家可能会给队友施加增益状态,以提高团队的战斗力。这些不同的玩家(具体观察者)根据自己的角色特点和游戏策略,对怪物出现(主题状态变化)做出了不同的响应,实现了各自独特的业务逻辑。
三、观察者模式工作流程
了解了观察者模式的组成部分后,接下来我们深入探讨一下它的工作流程,看看各个角色在其中是如何协同工作的。
3.1 注册观察者
当观察者对主题的状态变化感兴趣时,它会调用主题提供的注册方法,将自己添加到主题的观察者列表中 。这个过程就好比我们在电商平台上关注了某个商品,我们(观察者)通过点击 “关注” 按钮(调用注册方法),将自己的信息(观察者对象)添加到该商品(主题)的关注列表中。在代码实现上,以 Java 语言为例,假设我们有一个Subject接口和一个ConcreteSubject类,以及一个Observer接口和一个ConcreteObserver类:
// 主题接口
interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
// 具体主题类
class ConcreteSubject implements Subject {
private List<Observer> 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();
}
}
}
// 观察者接口
interface Observer {
void update();
}
// 具体观察者类
class ConcreteObserver implements Observer {
@Override
public void update() {
System.out.println("观察者收到通知并进行更新");
}
}
在上述代码中,ConcreteObserver想要观察ConcreteSubject的状态变化,它就会调用ConcreteSubject的registerObserver方法,将自己注册到ConcreteSubject的observers列表中。
3.2 状态改变
在系统运行过程中,当主题的内部状态或所管理的数据发生变化时,这就触发了观察者模式中的状态改变事件 。比如在电商平台中,之前关注的商品价格发生了变动,或者库存数量有了更新,这些都是主题状态的改变。在具体实现中,主题类通常会有一个或多个方法用于修改自身的状态,并且在状态修改完成后,准备通知观察者。例如在上面的ConcreteSubject类中,可以添加一个修改状态的方法:
class ConcreteSubject implements Subject {
// 省略其他代码...
private int state;
public void setState(int newState) {
this.state = newState;
// 状态改变后,调用通知方法
notifyObservers();
}
}
当调用setState方法改变state的值时,就表示主题的状态发生了变化,然后会立即调用notifyObservers方法,准备通知所有注册的观察者。
3.3 通知观察者
一旦主题的状态发生变化,它会执行通知方法,遍历观察者列表,并对每个注册的观察者调用其更新方法 。还是以电商平台为例,当商品价格改变后,电商平台系统就会遍历该商品的关注列表,向每一个关注了该商品的用户(观察者)发送价格变动的通知。在代码中,ConcreteSubject类的notifyObservers方法就是用于实现这个功能的:
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
这个方法会依次调用列表中每个Observer对象的update方法,从而通知观察者主题的状态已经发生了变化。
3.4 观察者更新
观察者接收到主题的通知后,会根据主题传递过来的状态信息或相关数据,进行相应的处理和更新操作,以保证自身状态与主题状态的一致性 。例如在电商平台中,用户(观察者)收到商品价格变动的通知后,可能会根据新价格决定是否购买该商品,或者重新评估自己的购物计划。在代码实现中,ConcreteObserver类的update方法就是观察者进行自身更新的地方:
class ConcreteObserver implements Observer {
@Override
public void update() {
System.out.println("观察者收到通知并进行更新");
// 这里可以添加具体的更新逻辑,比如获取主题的新状态并进行处理
}
}
在实际应用中,update方法内会包含具体的业务逻辑,根据不同的需求对主题的状态变化做出响应,可能是更新自身的显示界面、重新计算某些数据,或者执行其他相关操作。
四、观察者模式代码示例
通过前面的讲解,相信你已经对观察者模式的概念、组成和工作流程有了较为清晰的认识。接下来,我们通过具体的代码示例,分别用 Java 和 JavaScript 两种语言来实现观察者模式,以便你能更直观地理解它在实际编程中的应用。
4.1 Java 示例
首先,我们来看 Java 语言的实现。假设我们正在开发一个简单的新闻发布系统,其中新闻发布者是被观察者,订阅者是观察者。当有新的新闻发布时,发布者会通知所有的订阅者。
import java.util.ArrayList;
import java.util.List;
// 观察者接口,定义更新方法
interface Observer {
void update(String news);
}
// 具体观察者类,实现观察者接口
class Subscriber implements Observer {
private String name;
public Subscriber(String name) {
this.name = name;
}
@Override
public void update(String news) {
System.out.println(name + " 收到新闻: " + news);
}
}
// 主题接口,定义添加、删除和通知观察者的方法
interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObservers(String news);
}
// 具体主题类,实现主题接口
class NewsPublisher implements Subject {
private List<Observer> observers = new ArrayList<>();
@Override
public void attach(Observer observer) {
observers.add(observer);
}
@Override
public void detach(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String news) {
for (Observer observer : observers) {
observer.update(news);
}
}
// 发布新闻的方法
public void publishNews(String news) {
System.out.println("发布新闻: " + news);
notifyObservers(news);
}
}
// 测试类
public class ObserverPatternDemo {
public static void main(String[] args) {
// 创建新闻发布者
NewsPublisher publisher = new NewsPublisher();
// 创建订阅者
Subscriber subscriber1 = new Subscriber("张三");
Subscriber subscriber2 = new Subscriber("李四");
// 订阅者订阅新闻
publisher.attach(subscriber1);
publisher.attach(subscriber2);
// 发布者发布新闻
publisher.publishNews("观察者模式在Java中的应用");
// 订阅者2取消订阅
publisher.detach(subscriber2);
// 发布者再次发布新闻
publisher.publishNews("JavaScript中的设计模式");
}
}
在上述代码中:
-
Observer接口定义了update方法,当观察者接收到通知时会调用这个方法来更新自身状态。
-
Subscriber类实现了Observer接口,重写了update方法,具体实现了接收到新闻通知后的处理逻辑,即打印出订阅者的名字和收到的新闻内容。
-
Subject接口定义了管理观察者的方法,包括添加观察者(attach)、删除观察者(detach)和通知观察者(notifyObservers)。
-
NewsPublisher类实现了Subject接口,维护了一个观察者列表observers,并实现了接口中的方法。publishNews方法用于发布新闻,在发布新闻时会调用notifyObservers方法通知所有的观察者。
-
在ObserverPatternDemo测试类中,我们创建了一个新闻发布者和两个订阅者,订阅者订阅新闻后,发布者发布新闻,订阅者会收到通知。之后订阅者 2 取消订阅,发布者再次发布新闻时,订阅者 2 将不会收到通知。
4.2 JavaScript 示例
接下来,我们用 JavaScript 来实现观察者模式。同样以新闻发布系统为例:
// 定义主题类
class Subject {
constructor() {
this.observers = [];
}
// 添加观察者
addObserver(observer) {
this.observers.push(observer);
}
// 移除观察者
removeObserver(observer) {
this.observers = this.observers.filter(obs => obs!== observer);
}
// 通知所有观察者
notifyObservers(data) {
this.observers.forEach(observer => observer.update(data));
}
}
// 定义观察者类
class Observer {
constructor(name) {
this.name = name;
}
// 接收到通知时执行的更新方法
update(data) {
console.log(`${this.name} 收到通知: ${data}`);
}
}
// 使用示例
const newsPublisher = new Subject();
const subscriber1 = new Observer('王五');
const subscriber2 = new Observer('赵六');
newsPublisher.addObserver(subscriber1);
newsPublisher.addObserver(subscriber2);
newsPublisher.notifyObservers('JavaScript观察者模式实现');
newsPublisher.removeObserver(subscriber2);
newsPublisher.notifyObservers('设计模式在前端开发中的应用');
在这段 JavaScript 代码中:
-
Subject类代表主题,它维护了一个observers数组来存储所有的观察者。addObserver方法用于将观察者添加到数组中,removeObserver方法用于从数组中移除指定的观察者,notifyObservers方法则遍历观察者数组,调用每个观察者的update方法来通知它们。
-
Observer类代表观察者,它有一个name属性用于标识观察者的名字,update方法是当观察者接收到通知时执行的操作,这里是打印出观察者的名字和收到的通知内容。
-
在使用示例中,我们创建了一个新闻发布者newsPublisher和两个订阅者subscriber1、subscriber2,将订阅者添加到发布者的观察者列表中后,发布者通过notifyObservers方法发布通知,订阅者会收到通知。之后移除subscriber2,再次发布通知时,subscriber2将不会收到。
五、观察者模式的优缺点
5.1 优点
-
松耦合:观察者模式实现了对象之间的松耦合 。观察者和主题之间并不需要明确的依赖关系,它们只通过接口来交互,这使得系统更具扩展性。以电商平台为例,商品作为主题,用户作为观察者。当商品价格发生变化时,商品只需要通知所有注册的用户(观察者),而不需要关心每个用户具体会如何处理这个价格变化的信息。用户(观察者)也只需要关注商品价格变化的通知,而不需要了解商品的其他业务逻辑,两者之间通过抽象的接口进行交互,降低了彼此之间的耦合度,方便系统的维护和扩展。如果后续需要添加新的观察者,比如添加商家也关注商品价格变化,只需要让商家实现观察者接口并注册到商品主题中即可,不会影响到商品和其他已有的观察者的代码。
-
动态交互:可以在运行时添加或移除观察者,灵活性强 。这对于需要动态调整的程序非常有用。例如在游戏开发中,玩家可以随时关注或取消关注游戏中的某些事件(如怪物出现、道具刷新等)。在游戏运行过程中,当玩家进入某个特定区域时,系统可以动态地将该玩家注册为怪物出现事件的观察者,当怪物在该区域出现时,玩家就能收到通知并做出相应反应,比如准备战斗。而当玩家离开该区域时,又可以将其从怪物出现事件的观察者列表中移除,不再接收相关通知。这种动态交互的特性使得系统能够根据实际运行情况灵活地调整观察者与主题之间的关系,提高了系统的适应性和灵活性。
-
多种订阅方式:不同的观察者可以对同一主题订阅不同的条件,以应对复杂的业务逻辑 。继续以电商平台为例,对于商品库存变化这个主题,有的用户可能只关注商品库存低于某个阈值时的通知,以便及时购买;而商家可能关注商品库存高于某个数量时的通知,以便进行促销活动。通过观察者模式,不同的观察者可以根据自己的需求设置不同的订阅条件,主题在状态变化时,会根据这些条件有针对性地通知相应的观察者,满足了复杂业务场景下多样化的需求。
5.2 缺点
-
通知机制开销:在主题状态改变时,需要通知所有观察者,这可能导致性能问题,尤其是在观察者数量较多时 。比如在一个大型社交平台中,某个热门话题(主题)发生更新,可能有上百万的用户(观察者)关注了这个话题。当话题更新时,系统需要遍历并通知这上百万的用户,这个过程会消耗大量的时间和系统资源,可能导致系统响应变慢,甚至出现卡顿现象。为了应对这个问题,可以采用一些优化策略,例如批量通知、异步通知等。批量通知可以将多个观察者的通知操作合并成一次,减少通知的次数;异步通知则将通知操作放到后台线程中执行,避免影响主线程的正常运行,从而提高系统的性能和响应速度。
-
观察者管理复杂:观察者的管理可能会变得复杂,特别是在大量观察者需要注销的情况下,需要确保所有观察者的状态一致性 。假设在一个在线教育平台中,有大量学生(观察者)注册了某个课程(主题)的更新通知。当课程结束后,需要注销所有学生的观察者身份。如果在注销过程中出现部分学生注销失败,或者注销顺序不当导致某些学生仍然收到不必要的通知,就会出现观察者状态不一致的问题,影响用户体验。为了解决这个问题,需要建立完善的观察者管理机制,例如在注销观察者时进行状态检查和错误处理,确保每个观察者都能正确地被注销,同时记录注销操作的日志,以便在出现问题时进行追溯和排查。
-
隐式依赖:虽然观察者模式实现了松耦合,但观察者和主题之间仍然存在隐式依赖关系,可能导致在系统分析和维护时的复杂性增加 。例如在一个音乐播放应用中,播放列表(主题)和各个音乐播放器界面(观察者)之间通过观察者模式进行交互。当播放列表发生变化时,会通知各个播放器界面进行更新。然而,由于观察者和主题之间的依赖是通过接口实现的,在系统分析时,可能难以直观地看出哪些观察者依赖于哪个主题,以及它们之间具体的交互逻辑。在维护过程中,如果修改了主题的接口或者观察者的实现,可能会因为这种隐式依赖关系而引发一些难以察觉的错误。为了降低这种隐式依赖带来的复杂性,可以通过详细的文档记录观察者和主题之间的依赖关系和交互逻辑,同时在代码结构设计上尽量保持清晰和简洁,提高代码的可读性和可维护性。
六、观察者模式的应用场景
6.1 事件处理系统
在现代软件开发中,事件处理系统无处不在,而观察者模式在其中扮演着至关重要的角色。以 Web 前端开发中的 DOM 事件处理为例,当用户在网页上进行操作时,比如点击按钮、滚动页面、输入文本等,这些操作都会产生相应的事件。浏览器就像是一个主题,而开发人员编写的事件处理函数则是观察者。
当用户点击一个按钮时,按钮元素作为主题,会维护一个观察者列表,其中包含了所有为该按钮点击事件注册的处理函数。当按钮被点击(状态改变),按钮元素会遍历这个观察者列表,调用每个观察者的处理函数,从而实现对用户操作的响应。这种机制使得页面交互逻辑与页面元素本身的状态管理得以解耦,提高了代码的可维护性和扩展性。在后端开发中,许多框架也利用观察者模式来处理各种事件,如 Spring 框架中的事件监听机制。开发人员可以定义自定义事件和对应的监听器(观察者),当特定事件发生时,框架会自动通知相应的监听器进行处理,使得系统的各个模块之间能够通过事件进行松耦合的交互,增强了系统的灵活性和可扩展性。
6.2 数据绑定与 MVVM 模式
在前端开发领域,Vue 和 React 等框架的广泛应用使得数据绑定和 MVVM(Model - View - ViewModel)模式成为开发人员关注的焦点,而观察者模式正是这两者实现的重要基础。在 Vue 框架中,数据绑定是其核心特性之一。Vue 通过 Object.defineProperty () 方法对数据进行劫持,创建响应式数据。这些响应式数据就相当于主题,而依赖于这些数据的 DOM 元素或组件则是观察者。当响应式数据发生变化时,Vue 会自动通知所有依赖它的 DOM 元素或组件进行更新,从而实现数据与视图的自动同步。
在一个 Vue 组件中,我们定义了一个数据对象data,其中包含一个属性message。在模板中,我们通过插值表达式{{ message }}将message的值显示在页面上。此时,message就是主题,而包含插值表达式的 DOM 元素就是观察者。当message的值发生改变时,Vue 会自动检测到这个变化,并通知对应的 DOM 元素进行重新渲染,更新显示的内容,无需开发人员手动操作 DOM。在 MVVM 模式中,ViewModel 作为连接 Model 和 View 的桥梁,它一方面监听 Model 的变化,另一方面监听 View 的用户输入事件。当 Model 发生变化时,ViewModel 会通知 View 进行更新;反之,当 View 发生用户输入事件时,ViewModel 会将这些变化同步到 Model。这种双向数据绑定的实现,很大程度上依赖于观察者模式,使得 Model、View 和 ViewModel 之间实现了松耦合的高效协作,大大提高了前端开发的效率和代码的可维护性。
6.3 消息队列与发布 - 订阅系统
消息队列和发布 - 订阅系统在分布式系统、微服务架构等场景中被广泛应用,观察者模式为它们提供了重要的实现思路和基础机制。在消息队列中,生产者(发布者)将消息发送到消息队列中,而消费者(订阅者)则从消息队列中接收消息进行处理。消息队列就像是一个调度中心,它维护着生产者和消费者之间的关系。生产者无需知道具体有哪些消费者会接收消息,消费者也无需知道消息来自哪个生产者,它们通过消息队列实现了松耦合的通信。
以 Kafka 为例,Kafka 是一个分布式的消息发布 - 订阅系统,它允许多个生产者向一个主题(Topic)发送消息,同时允许多个消费者从这个主题订阅并消费消息。生产者将消息发布到指定的主题后,Kafka 会负责将这些消息分发给订阅了该主题的消费者。这种机制使得系统中的各个组件能够独立地进行扩展和维护,提高了系统的可伸缩性和可靠性。在分布式系统中,不同的微服务之间可能需要进行异步通信和事件通知,通过基于观察者模式的消息队列和发布 - 订阅系统,各个微服务可以实现高效的解耦通信,协同完成复杂的业务逻辑,促进了分布式系统的灵活性和稳定性。
七、总结
观察者模式通过定义对象间一对多的依赖关系,让一个对象状态变化时自动通知并更新依赖它的对象,实现了对象间的解耦 。它由主题、具体主题、观察者和具体观察者组成,工作流程包括注册观察者、状态改变、通知观察者以及观察者更新这几个关键步骤。在实际应用中,观察者模式优点明显,它实现了对象之间的松耦合,支持动态交互,还能满足多种订阅方式的需求,因此在事件处理系统、数据绑定与 MVVM 模式、消息队列与发布 - 订阅系统等众多场景中都有广泛应用。
当然,观察者模式也并非完美无缺,它存在通知机制开销大、观察者管理复杂以及存在隐式依赖等缺点 。但这并不影响它成为软件开发中一种极为重要的设计模式,只要我们在使用过程中充分了解其特性,合理运用,并针对其缺点采取相应的优化策略,就能充分发挥观察者模式的优势,提升软件系统的设计质量和可维护性。希望通过本文的介绍,你能对观察者模式有更深入的理解,并在今后的编程实践中灵活运用这一强大的设计模式。
更多推荐
所有评论(0)