设计模式学习笔记 - 设计模式与范式 -行为型:1.观察者模式(上)

概述

前面已经学习了创建型和结构性设计模式,从本章开始开始学习行为型设计模式。创建型设计模式主要解决 “对象的创建” 问题,结构性设计模式主要解决 “类或对象的组合或组装” 问题,行为型设计模式主要解决 “类或对象之间的交互” 问题。

行为型设计模式比较多,有 11 个,基于占了 23 种设计模式的一半。它们分别是:观察者模式、模版模式、策略模式、职责链模式、状态模式、迭代器模式、访问者模式、备忘录模式、命令模式、解释器模式、中介模式。

本章学习第一个行为型设计模式,也是应用的最广泛的一种设计模式:观察者模式。观察者模式有不同的代码实现方式:同步阻塞的实现方式、异步非阻塞的实现方式;进程内的实现方式,也有跨进程的实现方式。本章重点讲解原理、实现、应用场景。下章会实现一个基于观察者模式的异步非阻塞的 EventBus,加深你对这个模式的理解。


原理及应用场景剖析

观察者模式(Observer Design Pattern)也成为发布订阅模式(Publish-Subscribe Design Pattern)。GoF 的《设计模式》是这样定义的:

Define a one-to-many dependency between objects so that when one object changes state,all it’s dependents are notified and updated automatically.

翻译成中文:定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会收到通知。

一般情况下,被依赖的对象叫做被观察者(Observable),依赖的对象叫做观察者(Observer)。不过在实际的开发中,这两种对象的称呼比较灵活,比如:Subject-Observer、Publisher-Subscribe、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener。不管怎么称呼,只要应用场景符合刚刚的定义,都可以看做观察者模式。

实际上,观察者模式是一个比较抽象的模式,根据不同的应用场景,有完全不同的实现方式。现在,先来看其中最经典的一种实现方式。这也是在讲到观察者模式时,很多书籍给出的最常见的实现方式。其代码如下所示:

public interface Subject {void registerObserver(Observer observer);void removeObserver(Observer observer);void notifyObservers(Message message);
}public interface Observer {void update(Message message);
}public class ConcreteSubject implements Subject {private static List<Observer> observers = new ArrayList<>();@Overridepublic void registerObserver(Observer observer) {observers.add(observer);}@Overridepublic void removeObserver(Observer observer) {observers.remove(observer);}@Overridepublic void notifyObservers(Message message) {for (Observer observer : observers) {observer.update(message);}}
}public class ConcreteObserverOne implements Observer {@Overridepublic void update(Message message) {// 获取消息通知,执行自己的逻辑System.out.println("ConcreteObserverOne is notified.");}
}public class ConcreteObserverTwo implements Observer {@Overridepublic void update(Message message) {// 获取消息通知,执行自己的逻辑System.out.println("ConcreteObserverTwo is notified.");}
}public class Demo {public static void main(String[] args) {ConcreteSubject subject = new ConcreteSubject();subject.registerObserver(new ConcreteObserverOne());subject.registerObserver(new ConcreteObserverTwo());subject.notifyObservers(new Message());}
}

实际上,上面的代码算式观察者模式的 “模版代码”,只能反映大体的设计思路。在真实的软件开发中,并不需要照搬上面的代码。观察者模式的实现方法各式各样,函数、类的命名等会根据业务场景的不同有很大的差别,比如 register 函数还可以叫做 attachremove 函数还可以叫做 detach 等等。不过,万变不离其宗,设计思路都是差不多的。

原理和代码实现都非常简单,不需要过多解释。我们还是通过一个例子来重点讲下,什么情况下需要用到这种设计模式?

假设,要开发一个 P2P 投资理财系统,用户注册成功之后,我们会给用户发放投资体验金。代码实现大致是下面这样子的:

public class UserController {private UserService userService; // 依赖注入private PromotionService promotionService; //依赖注入public long register(String telephone, String password) {// 省略输入参数的校验代码// 省略userService.register()异常的try-catch代码long userId = userService.register(telephone, password);promotionService.issueNewUserExperienceCash(userId);return userId;}
}

虽然注册接口做了两件事情,注册和发放体验金,违反单一职责原则,但是如果没有扩展和修改的需求,现在的代码实现是可以接受的。如果非得用观察者模式,就需要引入更多的类和更加复杂的代码结构,反倒是一种过度设计。

如果需求频繁改动,比如用户注册成功之后,不在发放体验券,而是改为发放优惠券,并且还要给用户发送一封 “欢迎注册成功” 的站内信。这种情况下,就需要频繁地修改 register() 函数中的代码,违反开闭原则。而且,如果注册成功之后需要执行的后续操作越来越多,那 register() 函数的逻辑会变得越来越复杂,也就影响到代码的可读性和可维护性。

此时,观察者模式就派上用场了。利用观察者模式,对上面的代码进行重构。

public interface RegObserver {void handleRegSuccess(long userId);
}public class RegPromotionObserver implements RegObserver {private PromotionService promotionService; // 依赖注入@Overridepublic void handleRegSuccess(long userId) {promotionService.issueNewUserExperienceCash(userId);}
}public class RegNotificationObserver implements RegObserver {private NotificationService notificationService;@Overridepublic void handleRegSuccess(long userId) {notificationService.senInboxMessage(userId, "Welcome ...");}
}public class UserController {private List<RegObserver> regObservers = new ArrayList<>();private UserService userService; // 依赖注入// 一次性设置好,之后也不可能动态地修改public void setRegObservers(List<RegObserver> observers) {regObservers.addAll(observers);}public long register(String telephone, String password) {// 省略输入参数的校验代码// 省略userService.register()异常的try-catch代码long userId = userService.register(telephone, password);for (RegObserver observer : regObservers) {observer.handleRegSuccess(userId);}return userId;}
}

当我们需要添加观察者时,比如用户注册成功之后,推送用户注册信息给大数据征信系统,基于观察者模式的代码实现, UserController 类的 register() 函数完全不需要修改,只需要再添加一个实现 RegObserver 接口的类,并通过 setRegObservers() 函数将它注册到 UserController 类中即可。

前面已经学习了很多设计模式,不知道你发现没有,实际上,设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦,再具体到观察者模式,它是将观察者和被观察者解耦。 借助设计模式,利用更好的代码结构,将一大坨代码拆分成职责更单一的小类,让其满足开闭原则、高内聚松耦合等特性,依此来控制代码的复杂性,提高代码的可扩展性。

基于不同应用场景的不同实现方式

观察者模式的应用场景非常广泛,小到代码层面解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、Rss Feeds,本质上都是观察者模式。

不同的应用场景和需求下,这个模式也有截然不同的实现方式,上一小节我们提到,有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。

第一小节例子中的实现方式,是一种同步阻塞的实现方式。观察者和被观察者代码在同一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成之后,才执行后续的代码。对照上面讲到的用户注册的例子, register() 函数依次调用执行每个观察者的 handleRegSuccess() 函数,最后才返回结果给客户端。

如果注册接口是一个调用比较频繁地接口,对性能非常敏感,希望接口响应时间尽可能短,那我们可以将同步阻塞的实现方式改为异步非阻塞的实现方式,依此来减少响应时间。具体来讲,当 userService.register() 函数执行完成之后,我们启动一个新的线程开执行观察者的 handleRegSuccess() 函数,这样 userController.register() 函数不要等到所有的 handleRegSuccess() 函数都执行完成之后才返回结果给客户端。userController.register() 函数从执行 3 个 SQL 才返回,减少到只需要执行 1 个 SQL 语句就返回,响应时间粗略地讲减少为原来的 1/3

如何实现一个异步非阻塞的观察者模式呢?简单的做法是在每个 handleRegSuccess() 函数中,创建一个新线程执行代码。不过,我们还有更加优雅的实现方式,那就是基于 EventBus 来实现。

刚刚提到的两个场景,不管是同步阻塞实现方式还是异步非阻塞实现方式,都是进程内的实现方式。如果用户注册成功之后,我们需要发送用户信息给大数据征信系统,而大数据征信系统是一个独立的系统,跟它之间的交互是跨不同进程的,那如何实现一个跨进程的观察者模式呢?

如果大数据征信系统提供了发送用户注册信息的 RPC 接口,仍然可以沿用之前的实现思路,在 handleRegSuccess() 函数中调用 RPC 接口来发送数据。但是,我们还有更加优雅、更加常用的一种实现方式,那就是基于消息队列(比如 ActiveMQ)来实现。

当然,这种实现方式也有弊端,那就是需要引入一个新的系统(消息队列),增加了维护成本。不过,它的好处也非常明显。在原来的实现方式中,观察者需要注册到被观察者需要依次遍历观察者来发送消息。而基于消息队列的实现方式,被观察者和观察者解耦更加彻底,两部分的耦合更小。被观察者完全不感知观察者,同理,观察者也完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。

总结

设计模式要干的事情就是解耦,创建型模式是将创建和使用代码解耦,结构性模式是将不同功能代码解耦,行为性模式是将不同行为解耦,具体到观察者模式,它将观察者和被观察者代码解耦。借助设计模式,我们利用更好的代码结构,将一大坨代码拆分成职责更单一的小类,让其满足开闭原则、高内聚低耦合等特性,依此来控制和应对代码的复杂性,提高代码的可扩展性。

观察者模式的应用场景非常广泛,小到代码层面的解耦,大到框架层面的解耦,再或者一些产品的设计思路,都有观察者模式的影子,这个模式也有截然不同的实现方式,有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/784166.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

canvas+javascript 实现贪吃蛇游戏

引言 在当今数字化时代&#xff0c;编程已经成为一种极具创造力和趣味性的活动。通过编写代码&#xff0c;我们可以创造出各种各样的应用程序和游戏&#xff0c;其中包括经典的贪吃蛇游戏。本文将向您介绍如何使用 JavaScript 编程语言制作一个简单而有趣的贪吃蛇游戏&#xf…

Kafka(十一)管理Kafka

目录 管理Kafka1 命令行操作1.1 Topic操作1.1.1 创建主题1.1.2 列出集群所有主题1.1.3 列出主题详情1.1.4 增加主题分区数1.1.5 减少主题分区数1.1.6 删除主题 1.2 生产和消费1.2.1 控制台生产者1.2.2 控制台消费者 1.3 消费者群组1.3.1 列出并描述群组1.3.2 删除消费者群组1.3…

动态内存管理-错题合集讲解

空指针的解应用操作&#xff08;错误信息合集&#xff09; 越界访问 首先我们上一个代码&#xff0c;看看这个的代码的问题 这个代码的问题显而易见 &#xff0c;就是在循环里面&#xff0c;产生了越界访问的问题&#xff0c;这里你开辟了10个整形空间&#xff0c;但是从0-1…

【javaWeb 第九篇】功能接口开发流程以及常用注解

常用注解 准备-环境搭建开发规范开发流程 注解补充 准备-环境搭建 准备数据库表&#xff08;dept,emp&#xff09;准备后端SpringBoot环境 需要依赖&#xff1a; Web起步依赖&#xff0c;数据库驱动依赖&#xff0c;Mybatis依赖&#xff0c;lombok依赖配置文件application.pr…

9.Python类与对象

1 面向对象 类和对象都是面向对象中的重要概念。面向对象是一种编程思想&#xff0c; 即按照真实世界的思维方式构建软件系统。 例如&#xff0c;在真实世界的校园里有学生和老师&#xff0c;学生有学号、姓名、所 在班级等属性&#xff08;数据&#xff09;&#xff0c;还有…

标题:AI大模型学习:解放智能的未来之路

随着技术的不断进步和发展&#xff0c;AI大模型学习成为了当前人工智能领域的热点话题。AI大模型学习不仅仅需要研究者具备深厚的数学基础和编程能力&#xff0c;还需要对特定领域的业务场景有深入的了解。通过不断优化模型结构和算法&#xff0c;AI大模型学习能够不断提升模型…

MySQL核心命令详解与实战,一文掌握MySQL使用

文章目录 文章简介演示库表创建数据库表选择数据库删除数据库创建表删除表向表中插入数据更新数据删除数据查询数据WHERE 操作符聚合函数LIKE 子句分组 GROUP BY HAVINGORDER BY(排序) 语句LIMIT 操作符 分页查询多表查询-联合查询 UNION 操作符多表查询-连接的使用-JOIN语句编…

本地GPU调用失败问题解决3重新配置anaconda环境(成功)

1、右键“以管理员身份”打开anaconda prompt conda create -n python 3.9 2、使用官方下载源的配置 3、修改conda下载超时 conda config --set remote_connect_timeout_secs 60 conda config --set remote_read_timeout_secs 100 查看配置结果conda config --show 配置内…

122、内网安全——域信息收集应用网络凭据CS插件AdfindBloodHound

文章目录 理解域域信息搜集 理解域 假设有1000台计算机&#xff0c;运维人员需要为每一台计算机进行软件的安装、环境部署&#xff0c;实际上运维人员不可能亲自对每一台计算机进行软件的安装和环境部署。实际&#xff0c;将所有1000台计算机放入一个域中&#xff0c;域内有一…

多传感器标定——相机内参标定

文章目录 一、前言二、内参标定流程三、如何提升标定精度四、精度验证五、内外参联合标定 一、前言 之前写过一篇文章&#xff08;相机内参、外参、畸变系数简介&#xff09;&#xff0c;感觉应该把这几个东西说的还算明白&#xff0c;但是里边并没有深究该如何进行标定&#…

【软考---系统架构设计师】物联网和云计算

目录 一、物联网 二、云计算 一、物联网 物联网是实现物物相连的互联网络&#xff0c;其内涵包括两个方面&#xff1a;第一&#xff0c;物联网的核心和基础仍是互联网&#xff0c;是在互联网基础上延伸和扩展的网络&#xff1b;第二&#xff0c;其用户端延伸和扩展到了任何物…

牛客NC153 信封嵌套问题【中等 动态规划,最长递增子序列 Java,Go,PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/9bf77b5b018d4d24951c9a7edb40408f 相同的题目&#xff1a; https://www.lintcode.com/problem/602 思路 本质是求最长子序列问题envelopes 先按 w 升序排序&#xff0c;再按 h 降序 排序&#xff0c;只需考虑h…

一条SQL在MySQL中的执行过程

图解&#xff1a; 第⼀步&#xff1a;连接器 过程 1. 建⽴连接&#xff1a;与客户端进⾏ TCP 三次握⼿建⽴连接&#xff1b; 2. 校验密码&#xff1a;校验客户端的⽤户名和密码&#xff0c;如果⽤户名或密码不对&#xff0c;则会报错&#xff1b;3. 权限判断&#xff1a…

手机无线投屏到windows11电脑

1 安装无线投影组件 2 电脑端打开允许其他设备投影的开关 3 手机找到投屏选项 4 手机搜索可用设备连接即可 这里的官方文档给的不太好,给了一些让人眼花撩乱的信息,以下是经过整合的有效信息

Appium基础操作元素定位实操入门

一、基础操作 1、安装和配置:包括安装 Appium 服务器、设置环境变量等。2、启动服务器:使用命令行或相关工具启动 Appium 服务器。3、连接设备或模拟器:将测试设备或模拟器连接到计算机上。4、识别应用程序:指定要测试的应用程序的相关信息,如包名和活动名。上述1~4内容可…

金融衍生品市场

金融衍生品市场 衍生金融品的作用衍生金融工具远期合约期货合约期权 衍生金融品的作用 套期保值&#xff08;Hedging&#xff09; 组合多头头寸(long position)与空头头寸(short position)例&#xff1a;股票与股指期货 投机 衍生金融工具 远期合约 定义&#xff1a;在将来…

翻译: 硅谷软件工程师面试:准备所需的一切

没有人有时间去做成百上千道LeetCode题目&#xff0c;好消息是你实际上并不需要做那么多题目就能够在FAANG公司找到工作&#xff01; 我曾经在Grab工作&#xff0c;这是东南亚的一家共享出行公司&#xff0c;但我对工作感到沮丧&#xff0c;想要进入FAANG公司&#xff0c;但我…

【opencv】教程代码 —features2D(5)旋转相机的基本全景拼接

基本全景拼接 panorama_stitching_rotating_camera.cpp 将第二张图像进行透视变换后与第一张图像拼接 #include <iostream> // 包含了一些用于输入输出的函数 #include <opencv2/core.hpp> // 包含了OpenCV核心库的一些常用类和函数 #include <opencv2/imgpro…

ZooKeeper 宕机如何应对

ZooKeeper 本身也是集群&#xff0c;推荐配置奇数个服务器。因为宕机就需要选举&#xff0c;选举需要半数 1 票才能通过&#xff0c;为了避免打成平手。进来不用偶数个服务器。 如果是 Follower 宕机了&#xff0c;没关系不影响任何使用。用户无感知。如果 Leader 宕机&#x…

Android视角看鸿蒙第十课-鸿蒙的布局之线性布局

Android视角看鸿蒙第十课-鸿蒙的布局之线性布局 导读 这篇文章开始&#xff0c;依次学习鸿蒙的八大布局&#xff0c;这是第一篇&#xff0c;所以顺带也会聊聊通用属性。 文档地址 文档地址 如何定义一个线性布局 Android中是使用LinearLayout来构建线性布局的&#xff0c…