对观察者模式的理解

ops/2024/10/21 9:45:26/

目录

  • 一、场景
    • 1、题目描述 【[案例来源](https://kamacoder.com/problempage.php?pid=1075)】
    • 2、输入描述
    • 3、输出描述
    • 4、输入示例
    • 5、输出示例
  • 二、实现
  • 三、更复杂的场景 【[案例来源](https://refactoringguru.cn/design-patterns/observer/java/example#example-0--listeners-EmailNotificationListener-java)】
    • 1、简单实现
      • 1.1 可以改进的地方
    • 2、更优雅的实现
  • 四、个人思考

一、场景

  • 观察者模式是行为型模式之一。
  • 试想一下:
    • 学生们坐在教室上课,到了下课时刻,下课铃声响起,学生们听到铃声,进入课间休息时段。
      • 学生是观察者,观察对象是铃声通知器(一般挂在教室门口的墙上)。
  • 这种协作便是观察者模式

1、题目描述 【案例来源】

小明所在的学校有一个时钟(主题),每到整点时,它就会通知所有的学生(观察者)当前的时间,请你使用观察者模式实现这个时钟通知系统。
注意点:时间从 1 开始,并每隔一个小时更新一次。

2、输入描述

输入的第一行是一个整数 N(1 ≤ N ≤ 20),表示学生的数量。
接下来的 N 行,每行包含一个字符串,表示学生的姓名。
最后一行是一个整数,表示时钟更新的次数。

3、输出描述

对于每一次时钟更新,输出每个学生的姓名和当前的时间。

4、输入示例

2
Alice
Bob
3

5、输出示例

Alice 1
Bob 1
Alice 2
Bob 2
Alice 3
Bob 3

二、实现

  • 主题(Subject): 铃声通知器

主题状态(数据)变化后,通知订阅了该主题的观察者。

亦称:发布者(Publisher)
发布者状态(数据)变化后,通知订阅者(Subscriber)/观察者。

public interface Subject {void registerObserver(Observer observer);void removeObserver(Observer observer);void notifyObservers();
}public class TimeSubject implements Subject {private List<Observer> observers;@Setterprivate Integer startTime;public TimeSubject(Integer startTime) {this.startTime = startTime;this.observers = new ArrayList<>();}@Overridepublic void registerObserver(Observer observer) {observers.add(observer);}@Overridepublic void removeObserver(Observer observer) {observers.remove(observer);}@Overridepublic void notifyObservers() {for (Observer observer : observers) {observer.update(startTime);}startTime = (startTime + 1) % 24;}
}
  • 观察者/订阅者
public interface Observer {void update(Integer currentTime);
}public class StudentObserver implements Observer {private String name;public StudentObserver(String name) {this.name = name;}@Overridepublic void update(Integer currentTime) {System.out.println(name + " " + currentTime);}
}
  • 客户端
public class Application {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);TimeSubject timeSubject = new TimeSubject(1);int n = scanner.nextInt();for (int i = 0; i < n; i++) {String name = scanner.next();Observer studentObserver = new StudentObserver(name);timeSubject.registerObserver(studentObserver);}int frequency = scanner.nextInt();for (int i = 0; i < frequency; i++) {timeSubject.notifyObservers();}}

三、更复杂的场景 【案例来源】

  • 编辑器提供两种功能:打开文件 和 保存文件。
    • 当打开文件时,发邮件通知监听该行为的观察者。
    • 当保存文件时,会触发打印日志。

分析:

  • 客户端使用编辑器,当出现“打开文件”或者“保存文件”时,触发发送邮件或打印日志。
    • 很显然,编辑器是“铃声通知器”,是发布者。邮件监听、日志监听是观察者。

1、简单实现

  • 发布者
public interface Publisher {void registerObserver(String observeType, Observer observer);void removeObserver(String observeType, Observer observer);void notifyObservers(String observeType);
}public class Editor implements Publisher {private List<Observer> emailObservers;private List<Observer> logObservers;private File file;public Editor() {emailObservers = new ArrayList<>();logObservers = new ArrayList<>();}@Overridepublic void registerObserver(String observeType, Observer observer) {if ("open file".equals(observeType)) {emailObservers.add(observer);} else if ("save file".equals(observeType)) {logObservers.add(observer);} else {throw new RuntimeException("invalid observeType");}}@Overridepublic void removeObserver(String observeType, Observer observer) {if ("open file".equals(observeType)) {emailObservers.remove(observer);} else if ("save file".equals(observeType)) {logObservers.remove(observer);} else {throw new RuntimeException("invalid observeType");}}@Overridepublic void notifyObservers(String observeType) {ObserveContext observeContext = new ObserveContext().setObserveType(observeType).setFile(file);if ("open file".equals(observeType)) {emailObservers.stream().forEach(observer -> observer.update(observeContext));} else if ("save file".equals(observeType)) {logObservers.stream().forEach(observer -> observer.update(observeContext));} else {throw new RuntimeException("invalid observeType");}}public void openFile(String filePath) {if (StringUtils.isEmpty(filePath)) {throw new RuntimeException("filePath is empty");}this.file = new File(filePath);notifyObservers("open file");}public void saveFile() {if (null == this.file) {throw new RuntimeException("file is null");}notifyObservers("save file");}
}
  • 订阅者
public interface Observer {void update(ObserveContext context);
}public class EmailObserver implements Observer {private String email;public EmailObserver(String email) {this.email = email;}@Overridepublic void update(ObserveContext context) {String observeType = context.getObserveType();File file = context.getFile();Objects.requireNonNull(observeType, "observeType must not be null");Objects.requireNonNull(file, "file must not be null");System.out.println("send email to " + email + ", content: " + observeType + " " + file.getName());}
}public class LogObserver implements Observer {private String logFilePath;public LogObserver(String logFilePath) {this.logFilePath = logFilePath;}@Overridepublic void update(ObserveContext context) {String observeType = context.getObserveType();File file = context.getFile();Objects.requireNonNull(observeType, "observeType must not be null");Objects.requireNonNull(file, "file must not be null");System.out.println("save log to " + logFilePath + ", content: " + observeType + " " + file.getName());}
}
  • 发布者-订阅者,交互的数据
@Data
@Accessors(chain = true)
public class ObserveContext {private String observeType;private File file;
}

打开文件 or 保存文件

  • 客户端
public class Application {public static void main(String[] args) {Editor editor = new Editor();editor.registerObserver("open file", new EmailObserver("forrest@qq.com"));editor.registerObserver("save file", new LogObserver("/user/forrest/log/editor_log.txt"));editor.openFile("/user/forrest/file/test.txt");editor.saveFile();}
}/*
send email to forrest@qq.com, content: open file test.txt
save log to /user/forrest/log/editor_log.txt, content: save file test.txt
*/

1.1 可以改进的地方

  • Editor不符合单一原则。
    • 既有与编辑器相关的打开文件/关闭文件的API,又有与编辑器不相关的发布者逻辑。
  • 解决办法:对发布者逻辑进行封装。

2、更优雅的实现

  • 发布者
public interface Publisher {void registerObserver(String observeType, Observer observer);void removeObserver(String observeType, Observer observer);void notifyObservers(ObserveContext context); // 这里从`String observeType`变成了`ObserveContext context`(更灵活)
}// 对发布者逻辑进行封装。
public class PublisherManager implements Publisher {private static final Map<String, List<Observer>> OBSERVER_MAP = new HashMap<>();@Overridepublic void registerObserver(String observeType, Observer observer) {List<Observer> observerList = OBSERVER_MAP.get(observeType);if (observerList == null) {observerList = new ArrayList<>();OBSERVER_MAP.put(observeType, observerList);}observerList.add(observer);}@Overridepublic void removeObserver(String observeType, Observer observer) {List<Observer> observerList = OBSERVER_MAP.get(observeType);if (observerList != null) {observerList.remove(observer);}}@Overridepublic void notifyObservers(ObserveContext context) {List<Observer> observerList = OBSERVER_MAP.get(context.getObserveType());if (observerList != null) {for (Observer observer : observerList) {observer.update(context);}}}
}
  • 订阅者 + ObserveContext(和之前没变化)
    • 发布者和订阅者通过接口基于context进行交互,两者的耦合度极低。当发布者重构了,订阅者是无感知的。
  • Editor(组合了Publisher接口,逻辑很纯粹)
public class Editor {private static final Publisher publisherManager = new PublisherManager();private File file;public static Publisher getPublisherManager() {return publisherManager;}public void openFile(String filePath) {if (StringUtils.isEmpty(filePath)) {throw new RuntimeException("filePath is empty");}this.file = new File(filePath);ObserveContext observeContext = new ObserveContext().setObserveType("open file").setFile(file);publisherManager.notifyObservers(observeContext);}public void saveFile() {if (null == this.file) {throw new RuntimeException("file is null");}ObserveContext observeContext = new ObserveContext().setObserveType("save file").setFile(file);publisherManager.notifyObservers(observeContext);}
}
  • 客户端:
public class Application {public static void main(String[] args) {Editor editor = new Editor();Editor.getPublisherManager().registerObserver("open file", new EmailObserver("forrest@qq.com"));Editor.getPublisherManager().registerObserver("save file", new LogObserver("/user/forrest/log/editor_log.txt"));editor.openFile("/user/forrest/file/test.txt");editor.saveFile();}
}/*
send email to forrest@qq.com, content: open file test.txt
save log to /user/forrest/log/editor_log.txt, content: save file test.txt
*/

四、个人思考

  • 像“一、场景”中纯粹的发布者应该不多见,更常见的应该是“三、更复杂的场景”:一个应用,当其某些状态改变时(文件的打开或者关闭)会触发其他行为。这是观察者模式的用武之地。

http://www.ppmy.cn/ops/15514.html

相关文章

时间默认显示当前日期及系统时间

要将 xtdsSj 绑定到当前日期和系统时间&#xff0c;你可以在组件的 data 中初始化 xtdsSj 属性为当前日期及系统时间的字符串。然后&#xff0c;在组件创建时更新 xtdsSj&#xff0c;确保它始终显示当前日期和系统时间。 1.系统读数时间默认显示当前日期及系统时间 <templa…

自然语言生成软件!用码上飞CodeFlying来开发一个ChatBot

前言&#xff1a; 众所周知&#xff0c;2023年被称之为大模型的元年&#xff0c;随着ChatGPT的爆火&#xff0c;国内也涌现了诸多大模型的产品&#xff0c;从文生文、文生图片再到文生视频等多模态的应用成为了各家的主战场。但是在软件开发的领域&#xff0c;当前大部分的产品…

Flink Graph演变

1.概述 Flink 集群中运行的 Job&#xff0c;最终归根到底&#xff1a;还是构建一个高效能分布式并行执行的DAG执行图。一个 Flink 流式作业从 Client 提交到 Flink 集群到最后执行&#xff0c;总共经历 4 种状态&#xff0c;总体来说&#xff1a;Flink中的执行图可分成四层&…

使用excel文件生成sql脚本

目录 1、excel文件脚本变量2、公式示例 前言&#xff1a;在系统使用初期有一些基础数据需要从excel中导入到数据库中&#xff0c;直接导入的话可能有些字段用不上&#xff0c;所以就弄一个excel生成sql的导入脚本&#xff0c;这样可以将需要的数据填到指定的列即可生成sql。 1、…

Flask项目部署

1.debug模式 不用每次保存后重新运行&#xff08;热部署&#xff09; 看一下自己的ip ipconfig2.改host 可以让同一个局域网的人访问 3.修改port端口号 中间有空格

Xbox VR头盔即将推出,但它是Meta Quest的‘限量版’。

&#x1f4f3;Xbox VR头盔即将推出&#xff0c;但它是Meta Quest的‘限量版’。 微软与Meta合作推出限量版Meta Quest VR头映射Xbox风格&#xff0c;可能是Meta Quest 3或未来版本的特别定制版&#xff0c;附带Xbox控制器。这一合作是Meta向第三方硬件制造商开放其Quest VR头盔…

暗物质真的存在吗?暗物质和暗能量对宇宙学理论的挑战

暗物质和暗能量对宇宙学理论的挑战主要体现在以下几个方面&#xff1a; 普遍性和重要性&#xff1a;暗物质和暗能量在宇宙中的普遍性和重要性&#xff0c;以及目前对它们的理解仍然有限&#xff0c;对宇宙学理论提出了重大挑战。这些成分占据了宇宙总质量能量的大约95%&#xf…

Python学习从0开始——项目一day02数据库连接

Python学习从0开始——项目一day02数据库连接 一、在线云数据库二、测试数据库连接三、数据库驱动介绍四、SQL执行4.1插入测试数据4.2安装数据库连接模块4.3测试SQL语句执行4.4执行SQL的固定步骤及示例 一、在线云数据库 找了一个在线数据库&#xff0c;需要邮箱注册&#xff…