摸鱼被导师发现了-Java设计模式之观察者模式与发布-订阅模式

664 阅读24分钟

事实上,许多设计模式的思想在我们的日常生活中都有所体现,今天我们就从一个背景故事开始,学习今天的设计模式内容。

1,背景故事

小吴是一名硕士研究生,今天晚上和研究室的同学们一起吃饭时,便和大家分享起了今天下午摸鱼被抓的事情。

小吴:“今天好不容易处理完成了论文数据,一看时间才4点,心想着休息一下吧,于是打开了《甄嬛传》继续看了。没想到不知道什么时候,导师已经站在身后盯着我了,但是我还毫无察觉,据说老师盯了10分钟才叫了我,太吓人了!”

小李:“哈哈!你这背对着门是这样的,我也正在玩《原神》呢!但是我看见你导师来了,就马上把游戏关掉了,又不敢提醒你。”

小吴:“也是今天运气不好,平时坐在门口的小王同学总能够听见外面的情况,并及时告诉我,我们就可以立即停止摸鱼防止被老师发现了!结果今天小王同学被导师叫走去讨论新的课题去了,这下没有看门的同学,我就被抓个正着!好在我导师没有严厉批评我,只是提醒我下次不要在工作时间干别的事情。”

小李:“那还不错,咱们以后早上和下午还是老老实实工作,看论文吧,别老想着干别的事情了!”

2,观察者模式

事实上,上述的背景故事中就体现了一种设计模式:观察者模式(Observer Pattern),这是一种行为型设计模式,用于建立对象之间的一对多关系,使得当一个对象(被观察者)状态发生变化时,所有依赖于它的对象(观察者)都会得到通知并自动更新,这种模式常用于事件处理和数据变化的场景。

在上述背景故事场景中,被观察者都是老师(的行为),而观察者有几位学生(比如看电视剧的和打游戏的学生),他们观察到老师的行为发生改变(例如老师来了)后,就会立即停止摸鱼并切换回自己正在做正事(改变状态)。

(1) 组成部分

观察者模式主要由下列部分组成:

  • 抽象主题(Subject):被观察的对象,其中维护一个观察者列表,并在自身状态变化时通知观察者
  • 抽象观察者(Observer):观察主题的对象,其中有一个更新接口,以便当主题的状态发生变化时接收通知
  • 具体主题(Concrete Subject):实现主题接口,包含具体的状态等等
  • 具体观察者(Concrete Observer):实现观察者接口,包含接收到主题变化后更新的具体逻辑

(2) 代码实现

下面,我们就使用Java来实现一下上面的场景,借助观察者模式的思想,我们作类图如下:

可见:

  • 抽象主题:Subject抽象类,它通常必须要有下列属性和方法:
    • state属性:表示主题的状态,这里先用String类型
    • observers属性:表示观察这个主题的观察者列表
    • registerremove方法:增加和移除观察这个主题的观察者
    • notifyObservers方法:当自己的状态state更新时,提醒所有的观察者
  • 抽象观察者:Observer抽象类,它通常必须要有下列方法:
    • update方法:当观察主题更新时,这个方法会被调用以通知观察者,观察者接收到主题变化后更新自己状态或者做出反应,通常声明为抽象方法,由具体观察者实现
  • 具体主题:TeacherDean,可见这里同时模拟观察老师或者院长是否会来研究室查岗
  • 具体观察者:StudentWatchTVStudentPlayingGame,分别代表正在看电视剧和正在打游戏的学生
  • 客户端调用:GateKeeper类,为主类,调用观察者

对应的Java代码,首先是抽象观察者:

package com.gitee.swsk33.observerpatterndemo.model.prototype;

import lombok.Data;

/**
 * 抽象观察者,即观察主题变化的类
 */
@Data
public abstract class Observer {

	/**
	 * 名字
	 */
	private String name;

	/**
	 * 更新方法,被观测主题发生变化时,调用该方法传入更新状态后的主题,实现通知观察者,具体观察者通过该方法实现具体接受到通知后的逻辑
	 *
	 * @param subject 更新状态后的主题对象
	 */
	public abstract void update(Subject subject);

}

抽象主题(被观察对象):

package com.gitee.swsk33.observerpatterndemo.model.prototype;

import lombok.Data;

import java.util.ArrayList;
import java.util.List;

/**
 * 抽象主题,即被观察的对象
 */
@Data
public abstract class Subject {

	/**
	 * 主题名称
	 */
	private String name;

	/**
	 * 主题状态
	 */
	private String state;

	/**
	 * 全部观察这个主题的观察者列表
	 */
	private final List<Observer> observers = new ArrayList<>();

	/**
	 * 添加一个观察者对象到观察者列表
	 *
	 * @param observer 要添加的观察者对象
	 */
	public void register(Observer observer) {
		if (observer != null) {
			observers.add(observer);
		}
	}

	/**
	 * 从观察者列表移除一个观察者
	 *
	 * @param observer 要移除的观察者
	 */
	public void remove(Observer observer) {
		observers.remove(observer);
	}

	/**
	 * 当主题发生变化时,通知全部观察者
	 */
	public void notifyObservers() {
		for (Observer observer : observers) {
			observer.update(this);
		}
	}

}

具体观察者-看电视的学生:

package com.gitee.swsk33.observerpatterndemo.model;

import com.gitee.swsk33.observerpatterndemo.model.prototype.Observer;
import com.gitee.swsk33.observerpatterndemo.model.prototype.Subject;
import lombok.Data;

/**
 * 具体观察者:看电视剧的学生
 */
@Data
public class StudentWatchingTV extends Observer {

	/**
	 * 正在看的电视剧名称
	 */
	private String tvName;

	@Override
	public void update(Subject subject) {
		// 实现自定义的接受到消息后的逻辑
		System.out.printf("%s,发现%s正在%s,快关掉%s!\n", getName(), subject.getName(), subject.getState(), tvName);
	}

}

具体观察者-打游戏的学生:

package com.gitee.swsk33.observerpatterndemo.model;

import com.gitee.swsk33.observerpatterndemo.model.prototype.Observer;
import com.gitee.swsk33.observerpatterndemo.model.prototype.Subject;
import lombok.Data;

/**
 * 具体观察者:打游戏的学生
 */
@Data
public class StudentPlayingGame extends Observer {

	/**
	 * 正在玩的游戏名称
	 */
	private String gameName;

	@Override
	public void update(Subject subject) {
		// 实现自定义的接受到消息后的逻辑
		System.out.printf("%s,发现%s正在%s,不要玩%s了!\n", getName(), subject.getName(), subject.getState(), gameName);
	}

}

具体主题:老师和院长类目前没有其它属性,只需继承Subject类即可,这里就省略这两者的代码。

现在我们在主类调用一下整个观察者模式代码:

package com.gitee.swsk33.observerpatterndemo;

import com.gitee.swsk33.observerpatterndemo.model.Dean;
import com.gitee.swsk33.observerpatterndemo.model.StudentPlayingGame;
import com.gitee.swsk33.observerpatterndemo.model.StudentWatchingTV;
import com.gitee.swsk33.observerpatterndemo.model.Teacher;
import com.gitee.swsk33.observerpatterndemo.model.prototype.Subject;

/**
 * 主类:在门口的学生
 */
public class GateKeeper {

	public static void main(String[] args) {
		// 创建被观察主题对象
		Subject teacher = new Teacher();
		teacher.setName("张老师");
		teacher.setState("在办公室");
		Subject dean = new Dean();
		dean.setName("刘院长");
		dean.setState("在办公室");
		// 创建观察者对象
		StudentWatchingTV tvStudent = new StudentWatchingTV();
		tvStudent.setName("小吴");
		tvStudent.setTvName("甄嬛传");
		StudentPlayingGame gameStudent = new StudentPlayingGame();
		gameStudent.setName("小李");
		gameStudent.setGameName("原神");
		// 将观察者对象注册到每个主题
		teacher.register(tvStudent);
		teacher.register(gameStudent);
		dean.register(tvStudent);
		dean.register(gameStudent);
		// 主题对象改变,通知所有观察者
		teacher.setState("从走廊过来");
		teacher.notifyObservers();
		dean.setState("到研究室门口了");
		dean.notifyObservers();
	}

}

结果:

image-20241020112728199

当主题状态改变,并调用通知观察者后,所有的观察者都可以接收到对应的消息并做出响应。

在上述抽象主题类中,state属性表示主题的状态,而observers属性是观察这个主题的观察者列表,我们使用registerremove方法管理观察者列表,并在state改变时,使用notifyObservers方法通知所有观察者。

通知所有观察者的逻辑也很简单,就是遍历observers观察者列表,并调用每个观察者的update方法,这个update方法就是抽象观察者中的用于接收通知并更新状态的方法了!这里面由具体的观察者实现接收到主题变化通知后做出的具体操作的逻辑,通常使用update方法传入更新后的Subject主题对象,使得观察者能够得到观察的主题的新状态。

总的来说,观察者模式中对主题(被观察对象)和观察者两种类型进行了抽象,使得主题和观察者之间是松耦合的关系,这样我们无需关心具体的观察者是什么类型,或者说具体观察的主题是什么类型。但是主题和观察者两者仍然是有关联的,因为主题中维护了观察者列表,也就是说主题对象知道它的全部观察者。

观察者模式非常适合于需要建立松耦合的事件处理机制的场景,比如GUI事件处理、数据模型变化通知等。

事实上,JDK内部也提供了关于观察者的类ObserverObservable类能让我们直接实现观察者模式,不过这两个类在JDK 9开始就被弃用了,大家可以单独去了解一下。

3,发布-订阅模式

发布-订阅模式(Publish-Subscribe Pattern) 是一种行为型设计模式,它是观察者模式的一种扩展,由于和观察者模式非常类似,且都用于事件监听处理的这种大场景下,因此许多人可能会将发布-订阅模式和观察者模式混淆,事实上它们是两种不同的设计模式。

在发布-订阅模式中,不再是主题(被观察者)直接通知观察者,而是通过一个中间件:通常是称为事件总线(Event Bus)消息队列(Broker, Message Queue) 的组件来进行解耦。

在发布-订阅模式中,发布者将发布事件到消息总线,消息总线再将消息广播给订阅者,此外消息总线还可能会对消息进行一定的处理等等,并且可以把消息分成不同的主题(Topic),不同的订阅者订阅不同的主题,这样消息总线只会把对应的主题的消息广播给对应的订阅者。

(1) 组成部分

在发布-订阅模式中,通常有如下组成部分:

  • 抽象发布者(Abstract Publisher):用于发布事件或者消息到事件总线
  • 抽象订阅者(Abstract Subscriber):从事件总线订阅并接受它所订阅的主题的消息
  • 事件/消息(Event):发布者发布的消息对象,通常包含主题和消息内容这两个属性
  • 事件总线(Event Bus / Broker):用于接收发布者的消息,并广播给订阅者,其中维护一个不同主题对应的不同的订阅者的列表,起到一个对消息的总体调度的作用
  • 具体发布者(Concrete Publisher):实现抽象发布者的发布事件逻辑
  • 具体订阅者(Concrete Subscriber):实现抽象订阅者接收到事件并处理事件的逻辑

可见在发布-订阅模式中,观察者模式的Subject概念被拆分为了PublisherEvent,也就是说被观察者的变化单独抽象成了Event对象,发布者Publisher只需通过事件总线传递消息Event给订阅者Subscriber即可,订阅者Subscriber就对应观察者模式中的观察者Observer,都是接收变化的角色。

image-20241020132245179

这样,被观察者和观察者不再是直接耦合了,并且发布者和订阅者也只需要关心事件的传递即可,发布者和订阅者被进一步地解耦合了,也就是说发布者和订阅者都无需知道对方各自的信息,只专注于事件的传递与接收即可,它们都依赖于事件总线,大幅提升了发布和订阅双方的灵活性与可扩展性。发布-订阅模式很好地体现了软件设计中的 依赖倒置(Dependency Inversion) 原则,两者不再互相依赖,而是都依赖于一个高层模块。

(2) 代码实现

这里我们在借助发布-订阅模式,使用Java来实现一下文章开头的背景故事,我们作类图如下:

可见:

  • 事件/消息:Event类,通常需要包含:
    • topic属性:表示这个事件对应的主题
    • data属性:表示这个事件的内容
  • 事件总线:Broker类,通常需要包含下列属性或者方法:
    • subscriberMap属性:维护所有不同主题的订阅者列表,为Map类型,键表示主题,值表示这个主题对应的订阅者列表
    • subscribeunsubscribe方法:添加或者移除某个主题的订阅者
    • publish方法:发布事件,根据事件的主题,发布事件对象给所有订阅这个主题的订阅者并通知
  • 抽象发布者:Publisher抽象类,通常需要包含:
    • broker属性:发布者通常发布事件给事件总线,因此broker表示这个发布者发布事件的事件总线对象
    • publish方法:将事件发布到事件总线,一般来说直接调用它自己的broker属性进行发布即可
  • 抽象订阅者:Subscriber接口,通常包含:
    • onSubscribe方法:订阅到事件后的逻辑,由子类实现
  • 具体发布者:GateKeeper类,故事场景中的“看门”同学,用于发布关于老师的事件
  • 具体订阅者:StudentWatchingTVStudentPlayingGame类,故事场景中看电视剧和打游戏的同学,接收关于老师的事件

对应的代码,首先是事件对象:

package com.gitee.swsk33.publishsubscribedemo.model;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * 事件对象
 *
 * @param <T> 事件内容的具体类型
 */
@Data
@AllArgsConstructor
public class Event<T> {

	/**
	 * 事件主题,用于区分不同的订阅频道
	 */
	private String topic;

	/**
	 * 事件内容
	 */
	private T data;

}

抽象订阅者接口:

package com.gitee.swsk33.publishsubscribedemo.model.prototype;

import com.gitee.swsk33.publishsubscribedemo.model.Event;

/**
 * 抽象订阅者接口
 */
public interface Subscriber {

	/**
	 * 订阅到事件后的逻辑,由子类实现
	 *
	 * @param event 订阅到的事件
	 */
	void onSubscribe(Event<?> event);

}

事件总线类:

package com.gitee.swsk33.publishsubscribedemo.model;

import com.gitee.swsk33.publishsubscribedemo.model.prototype.Subscriber;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 事件总线<br>
 * 维护所有的主题对应的订阅者列表,并负责消息传递
 */
public class Broker {

	/**
	 * 存放每个时间话题对应的订阅者列表的哈希表
	 * <ul>
	 *     <li>键:话题,Topic</li>
	 *     <li>值:订阅这个话题的订阅者列表</li>
	 * </ul>
	 */
	private final Map<String, List<Subscriber>> subscriberMap;

	public Broker() {
		subscriberMap = new HashMap<>();
	}

	/**
	 * 增加一个订阅者
	 *
	 * @param topic      订阅的话题
	 * @param subscriber 订阅者对象
	 */
	public void subscribe(String topic, Subscriber subscriber) {
		if (!subscriberMap.containsKey(topic)) {
			subscriberMap.put(topic, new ArrayList<>());
		}
		subscriberMap.get(topic).add(subscriber);
	}

	/**
	 * 移除一个订阅者
	 *
	 * @param topic      订阅的话题
	 * @param subscriber 订阅者对象
	 */
	public void unsubscribe(String topic, Subscriber subscriber) {
		if (subscriberMap.containsKey(topic)) {
			subscriberMap.get(topic).remove(subscriber);
		}
	}

	/**
	 * 发布一个事件
	 *
	 * @param event 发布的事件对象
	 */
	public void publish(Event<?> event) {
		// 获取订阅这个事件话题的全部订阅者
		List<Subscriber> subscribers = subscriberMap.get(event.getTopic());
		for (Subscriber subscriber : subscribers) {
			// 传递事件对象给订阅者
			subscriber.onSubscribe(event);
		}
	}

}

抽象发布者:

package com.gitee.swsk33.publishsubscribedemo.model.prototype;

import com.gitee.swsk33.publishsubscribedemo.model.Broker;
import com.gitee.swsk33.publishsubscribedemo.model.Event;

/**
 * 抽象发布者
 */
public abstract class Publisher {

	/**
	 * 发布到该事件总线
	 */
	private final Broker broker;

	/**
	 * 构造函数
	 *
	 * @param broker 传入事件总线对象
	 */
	public Publisher(Broker broker) {
		this.broker = broker;
	}

	/**
	 * 发布事件
	 *
	 * @param event 要发布的事件对象
	 */
	public void publish(Event<?> event) {
		broker.publish(event);
	}

}

具体订阅者-看电视的同学:

package com.gitee.swsk33.publishsubscribedemo.model;

import com.gitee.swsk33.publishsubscribedemo.model.prototype.Subscriber;
import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * 具体订阅者:正在看电视剧的同学
 */
@Data
@AllArgsConstructor
public class StudentWatchingTV implements Subscriber {

	/**
	 * 名字
	 */
	private String name;

	/**
	 * 电视剧名字
	 */
	private String tvName;

	@Override
	public void onSubscribe(Event<?> event) {
		// 实现自定义的订阅到消息后的逻辑
		System.out.printf("[%s] 接收到:%s,关掉%s...\n", name, event.getData(), tvName);
	}

}

具体订阅者-打游戏的同学:

package com.gitee.swsk33.publishsubscribedemo.model;

import com.gitee.swsk33.publishsubscribedemo.model.prototype.Subscriber;
import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * 具体订阅者:正在打游戏的学生
 */
@Data
@AllArgsConstructor
public class StudentPlayingGame implements Subscriber {

	/**
	 * 名字
	 */
	private String name;

	/**
	 * 游戏名
	 */
	private String gameName;

	@Override
	public void onSubscribe(Event<?> event) {
		// 实现自定义的订阅到消息后的逻辑
		System.out.printf("[%s] 接收到:%s,不要玩%s了!\n", name, event.getData(), gameName);
	}

}

具体发布者-门神同学:

package com.gitee.swsk33.publishsubscribedemo.model;

import com.gitee.swsk33.publishsubscribedemo.model.prototype.Publisher;

/**
 * 具体发布者:在门口的同学
 */
public class GateKeeper extends Publisher {

	/**
	 * 构造函数
	 *
	 * @param broker 传入事件总线对象
	 */
	public GateKeeper(Broker broker) {
		super(broker);
	}

}

现在我们在主类客户端调用一下整个发布-订阅模式,模拟故事场景:

package com.gitee.swsk33.publishsubscribedemo;

import com.gitee.swsk33.publishsubscribedemo.model.*;
import com.gitee.swsk33.publishsubscribedemo.model.prototype.Publisher;
import com.gitee.swsk33.publishsubscribedemo.model.prototype.Subscriber;

/**
 * 主类客户端调用
 */
public class Main {

	public static void main(String[] args) throws Exception {
		// 话题
		final String TOPIC_TEACHER = "teacher";
		final String TOPIC_DEAN = "dean";
		// 事件总线
		Broker broker = new Broker();
		// 消息订阅者
		Subscriber tvStudent = new StudentWatchingTV("小吴", "甄嬛传");
		Subscriber gameStudent = new StudentPlayingGame("小李", "原神");
		// 订阅消息,一个人订阅“老师”话题,一个人订阅“院长”话题
		broker.subscribe(TOPIC_TEACHER, tvStudent);
		broker.subscribe(TOPIC_DEAN, gameStudent);
		// 延迟一小会
		System.out.println("大家正在摸鱼...");
		Thread.sleep(1000);
		// 发布者发布消息
		Publisher gateStudent = new GateKeeper(broker);
		gateStudent.publish(new Event<>(TOPIC_TEACHER, "张老师正在走廊上过来了"));
		gateStudent.publish(new Event<>(TOPIC_DEAN, "刘院长已经到研究室门口看着了"));
	}

}

结果:

image-20241020133814832

可见调用的时候,只需要先让订阅者订阅对应主题的消息,发布者再发布消息即可,订阅者接收到消息就能够做出对应的处理。

对于事件对象的处理,都是统一由消息总线Broker完成,在Broker中使用subscribeunsubscribe方法管理对应主题的对应订阅者,然后使用publish方法广播事件给对应主题的订阅者,即根据事件的主题,获取这个主题全部的订阅者列表,并遍历该列表调用订阅者的onSubscribe方法实现把事件传递给订阅者,具体的订阅者在onSubscribe方法中也实现了自己具体接收到事件后处理事件的逻辑。

到此,我们就简单地实现了发布-订阅模式,事实上上述代码还有可以改进的地方,比如我们可以在Broker中使用队列存放事件实现异步发布-订阅,以及使用双检锁改进subscribeunsubscribe方法保证线程安全等等。

发布-订阅模式事实上也广泛地应用于各种分布式系统的开发中,例如RabbitMQ、Kafka等这样的消息队列就是系统中的Broker的角色,不同的系统模块可以借助消息队列发布和订阅消息。

4,使用Reactor实现发布-订阅模式

在Java中事实上已经有一些成熟的外部库已经实现了发布-订阅模式了,例如Reactor等等。

Reactor是一个反应式编程的范式的实现,反应式编程(或者称作响应式编程)是一种处理数据流和变化传递的一种异步编程范式,这意味着可以使用编程语言轻松处理静态的批量数据(例如数组和列表)以及动态的数据源(例如会持续产生数据的事件发射器)产生的数据流。简单地说,Reactor中使用发布-订阅的方式来异步地处理数据流,并最终由订阅者接收。

在项目中集成Reactor依赖,我们需要使用Java 8及其以上版本,我们首先在pom.xmlproject节点中添加一个dependencyManagement节点加入依赖管理信息:

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>io.projectreactor</groupId>
			<artifactId>reactor-bom</artifactId>
			<version>2023.0.8</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>

然后再在dependencies节点中加入reactor-core依赖即可:

<!-- Reactor核心库 -->
<dependency>
	<groupId>io.projectreactor</groupId>
	<artifactId>reactor-core</artifactId>
</dependency>

(1) 简单示例

我们来使用Reactor简单地还原一下上述故事场景:

package com.gitee.swsk33.publishsubscribereactordemo;

import com.gitee.swsk33.publishsubscribereactordemo.model.Event;
import reactor.core.publisher.Flux;

/**
 * 主类客户端调用<br>
 * 发布后立即订阅的场景
 */
public class SubscribeImmediately {

	public static void main(String[] args) throws Exception {
		// Flux对象,用于传递事件给订阅者
		// “老师”话题发布者
		Flux<Event<String>> teacherFlux = Flux.create(sink -> {
			// create方法编写发布元素的逻辑,sink参数是FluxSink类型对象,用于操纵元素的发布等等
			// next方法发布一个元素
			sink.next(new Event<>("teacher", "张老师正在走廊上"));
			// complete方法结束元素发布,告诉订阅者已完成全部元素发布
			sink.complete();
		});
		// “院长”话题发布者
		Flux<Event<String>> deanFlux = Flux.create(sink -> {
			sink.next(new Event<>("dean", "院长已经在研究室门口看着了"));
			sink.complete();
		});
		// 延迟一会
		System.out.println("大家正在摸鱼...");
		Thread.sleep(1000);
		// 订阅Flux
		teacherFlux.subscribe(event -> {
			// 订阅“老师”话题的学生订阅逻辑
			System.out.printf("[%s] 接收到:%s,关掉%s...\n", "小吴", event.getData(), "甄嬛传");
		});
		deanFlux.subscribe(event -> {
			// 订阅“院长”话题的学生订阅逻辑
			System.out.printf("[%s] 接收到:%s,不要玩%s了!\n", "小李", event.getData(), "原神");
		});
	}

}

结果:

image-20241020140638062

结合代码,我们来认识一下Reactor中的一些概念。

首先是Flux对象,这个对象是Reactor中的核心概念之一,它只是一个产生元素的发生器对象,通常它兼具发布者事件总线的作用,自身不包含数据,它的泛型类型就是发布的元素类型,该对象支持链式调用一些操作符方法,实现发布元素、中间处理元素以及最后订阅的逻辑,以下是Flux对象的工作流程:

image-20240721004424854

在上图中,横轴是时间轴,可见在一定时间内(横轴从左到右),Flux对象产生了多个数据元素(图中的彩色圆球),纵向从上至下则表示通过一系列的操作符对数据进行转换的过程,到最下面则是转换后的数据流元素,它们可以被消费者订阅。

在使用Flux时,我们可以将整个过程想象成一个生产线或者流水线,其中:

  • 发布逻辑位于生产线的最开始的部分,也称之为上游,它会源源不断地产生原始材料(原始数据元素)
  • 操作符是生产线中每一个装配台或者工作台,它们会对开始的原材料进行加工,处理
  • 订阅者则是生产线最末端,也称之为下游,它接收经过全部加工(全部操作符)后的工件(操作符处理后的数据元素),然后进行最后的使用或者处理
  • 除此之外,错误处理机制可以自定义我们在操作符处理错误时那个元素我们怎么处理,以及是继续处理生产线上后面的元素还是终止生产线
  • 还有背压机制,比如末端的订阅者决定元素太多了,它可以向最开始的发布者发送信号,让它停止生产线一会,或者是限制上游速度

在上述代码中,我们调用Fluxcreate方法,通过Lambda表达式自定义了发布者发布事件的逻辑,而在最后使用subscribe方法自定义了订阅者订阅到事件后的逻辑,整个发布-订阅的过程都是异步的。

image-20241020143922558

事实上,Reactor中的发布-订阅模式是懒加载的,也就是说一个Flux在第一个订阅者订阅之前无事发生,即上述的create方法只是定义了发布者发布元素的逻辑,并不是说一调用create就立即发布出元素了,当有订阅者开始订阅时,才会实际地发布元素。

(2) 不定期发布-订阅

在上述示例中,订阅者一订阅就立即接收到事件了!那么能否先让订阅者订阅事件,后续不定期发布事件呢?也是可以的。

我们先来看一下上述create中的sink参数,它是FluxSink类型,这个对象就是用于操纵发布相关的操作的对象,上述我们用到了它的如下方法:

  • next 发布一个元素
  • complete 通知所有订阅者,全部元素已发布完成,整个发布进程结束

这样,我们可以封装一个发布者类型,在其中把sink封装一下,利用create方法把sink传给发布者,后续再使用发布者调用发布。

此外,除了通过Lambda表达式方式传入自定义的订阅数据逻辑之外,我们还可以单独创建一个订阅者类,并在其中实现订阅数据的逻辑,我们只需继承Reactor提供的BaseSubscriber类型,并重写部分方法即可,这个类型可以理解为Reactor给我们提供的抽象订阅者类,我们需要重写它的如下方法:

  • hookOnSubscribe 开始订阅时该方法会被执行,在其中我们使用request(1)在最开始请求订阅一个元素,否则不会进行订阅操作
  • hookOnNext 每次进行订阅操作时该方法会被执行,其中参数即为本次订阅得到的元素,我们可以在该方法中自定义处理订阅元素的逻辑,此外在最后我们仍然需要调用request(1)请求订阅下一个元素,否则就不会继续订阅了

依据这个思路,我们来进一步还原一下故事场景,作类图如下:

事件对象和之前一样,具体发布者-门神同学代码如下,即我们自己封装sink的发布者类:

package com.gitee.swsk33.publishsubscribereactordemo.model.publisher;

import com.gitee.swsk33.publishsubscribereactordemo.model.Event;
import reactor.core.publisher.FluxSink;

/**
 * 具体发布者:在门口的同学
 */
public class GateKeeper {

	/**
	 * FluxSink是Flux对象异步发布元素时,用于操作元素发布的对象
	 */
	private FluxSink<Event<String>> sink;

	/**
	 * 设定sink对象
	 *
	 * @param sink FluxSink对象
	 */
	public void setupSink(FluxSink<Event<String>> sink) {
		this.sink = sink;
	}

	/**
	 * 发布一个事件
	 *
	 * @param event 发布的事件对象
	 */
	public void publish(Event<String> event) {
		if (sink != null) {
			sink.next(event);
		}
	}

}

具体订阅者-看电视的同学:

package com.gitee.swsk33.publishsubscribereactordemo.model.subscriber;

import com.gitee.swsk33.publishsubscribereactordemo.model.Event;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.reactivestreams.Subscription;
import reactor.core.publisher.BaseSubscriber;

/**
 * 具体订阅者:正在看电视剧的同学
 */
@Data
@AllArgsConstructor
public class StudentWatchingTV extends BaseSubscriber<Event<String>> {

	/**
	 * 名字
	 */
	private String name;

	/**
	 * 电视剧名字
	 */
	private String tvName;

	/**
	 * 订阅开始时操作
	 */
	@Override
	public void hookOnSubscribe(Subscription subscription) {
		System.out.printf("[%s]准备订阅消息!\n", name);
		// 请求订阅一个元素
		request(1);
	}

	/**
	 * 每次订阅到元素的操作
	 *
	 * @param value 订阅到的元素
	 */
	@Override
	public void hookOnNext(Event<String> value) {
		System.out.printf("[%s]接收到:%s,关掉%s...\n", name, value.getData(), tvName);
		// 继续请求订阅下一个元素
		request(1);
	}

}

具体订阅者-打游戏的同学:

package com.gitee.swsk33.publishsubscribereactordemo.model.subscriber;

import com.gitee.swsk33.publishsubscribereactordemo.model.Event;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.reactivestreams.Subscription;
import reactor.core.publisher.BaseSubscriber;

/**
 * 具体订阅者:正在打游戏的学生
 */
@Data
@AllArgsConstructor
public class StudentPlayingGame extends BaseSubscriber<Event<String>> {

	/**
	 * 名字
	 */
	private String name;

	/**
	 * 游戏名
	 */
	private String gameName;

	/**
	 * 订阅开始时操作
	 */
	@Override
	public void hookOnSubscribe(Subscription subscription) {
		System.out.printf("[%s]准备订阅消息!\n", name);
		// 请求订阅一个元素
		request(1);
	}

	/**
	 * 每次订阅到元素的操作
	 *
	 * @param value 订阅到的元素
	 */
	@Override
	public void hookOnNext(Event<String> value) {
		System.out.printf("[%s]接收到:%s,不要玩%s了!\n", name, value.getData(), gameName);
		// 继续请求订阅下一个元素
		request(1);
	}

}

好的,现在修改主类客户端调用如下:

package com.gitee.swsk33.publishsubscribereactordemo;

import com.gitee.swsk33.publishsubscribereactordemo.model.Event;
import com.gitee.swsk33.publishsubscribereactordemo.model.publisher.GateKeeper;
import com.gitee.swsk33.publishsubscribereactordemo.model.subscriber.StudentPlayingGame;
import com.gitee.swsk33.publishsubscribereactordemo.model.subscriber.StudentWatchingTV;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Flux;

/**
 * 主类客户端调用<br>
 * 不定期发布的场景
 */
public class IrregularPublish {

	public static void main(String[] args) throws Exception {
		// 两个看门的同学,一个看老师,一个看院长(具体发布者)
		GateKeeper teacherWatcher = new GateKeeper();
		GateKeeper deanWatcher = new GateKeeper();
		// Flux对象,用于传递事件给订阅者
		// “老师”话题发布者
		Flux<Event<String>> teacherFlux = Flux.create(sink -> {
			// create方法编写发布元素的逻辑,sink参数是FluxSink类型对象,用于操纵元素的发布等等
			// 这里先把sink传递给具体发布者即可,后续具体发布者再调用sink发布元素
			teacherWatcher.setupSink(sink);
		});
		// “院长”话题发布者
		Flux<Event<String>> deanFlux = Flux.create(sink -> {
			deanWatcher.setupSink(sink);
		});
		// 两个订阅者
		BaseSubscriber<Event<String>> tvStudent = new StudentWatchingTV("小吴", "甄嬛传");
		BaseSubscriber<Event<String>> gameStudent = new StudentPlayingGame("小李", "原神");
		// 一个订阅“老师”话题,一个订阅“院长”话题
		teacherFlux.subscribe(tvStudent);
		deanFlux.subscribe(gameStudent);
		// 延迟一小段时间
		System.out.println("大家正在摸鱼...");
		Thread.sleep(1000);
		// 发布消息
		teacherWatcher.publish(new Event<>("teacher", "张老师正在走廊上"));
		System.out.println("老师走了,继续摸鱼...");
		Thread.sleep(800);
		deanWatcher.publish(new Event<>("dean", "院长已经来研究室门口看着了"));
	}

}

结果:

image-20241020143148030

通过上述代码,我们成功地借助Reactor中的一些类,实现了不定期的发布-订阅模式,可见在调用sink参数的complete方法之前,我们可以一直发布元素,订阅者也可以一直订阅元素。

这里我们只是学习了Reactor的最基本用法,更多内容大家可以参考其官方文档:传送门

5,总结

通过一个背景故事,我们同时学习和实现了观察者模式与发布-订阅模式这两种设计模式,可见它们各有优缺点,都适合在事件处理、监听等场景下使用。

两种设计模式可以总结如下:

  • 观察者模式:定义了观察者(Observer)和主题(Subject)两大类对象,观察者直接依赖于主题,主题对象通常会维护一个观察者列表,并且当主题对象的状态发生变化时,它会直接通知这些观察者,实现观察者对主题的变化监听和感知
  • 发布-订阅模式:定义了发布者(Publisher)、订阅者(Subscriber)、事件/消息(Event)和事件中心(Event Bus、Broker或Message Queue),发布者不需要知道哪些订阅者存在,它只是发布事件到事件中心,同样订阅者不需要知道发布者的具体信息,它们只关心订阅的事件类型

对比两种设计模式:

设计模式耦合度复杂度侧重点
观察者模式松耦合,但仍有一定耦合度简单主题变化、对观察者通知和观察者的自我更新
发布-订阅模式完全解耦合较为复杂事件的传递分发与订阅者对事件的处理

本文示例代码仓库: