观察者模式的理解和引用

1.前言

在之前的H5小游戏中,对于长连接发送的不同类型数据包的处理,是通过switch语句进行处理的,于是在自己的代码中出现了大量的case分支,不方便进行维护和后期的版本迭代。于是在老师的指导下,开始寻求使用观察者模式来解决case分支过多、代码冗余的问题。

H5小游戏介绍和代码仓库:基于WebSocket通信的H5小游戏总结-CSDN博客

2.旧代码

		//在信息中枢处根据消息类型进行特定的处理switch requestPkg.Type {case pojo.CertificationType://用户认证client.CertificationProcess(requestPkg)case pojo.CreateRoomType://创建房间号,并将创建者加入房间client.CreateRoomProcess()case pojo.JoinRoomType://1.加入房间的前提,先建立连接//2.完成用户认证//3.发送消息类型和房间号 Type uuid//只有完成上述步骤,才可以加入房间var data map[string]interface{}err = json.Unmarshal([]byte(requestPkg.Data), &data)if err != nil {fmt.Println("解析 JSON 失败:", err)return}uuidValue, ok := data["uuid"].(string)if !ok {fmt.Println("uuid 字段不存在或不是字符串类型")return}client.JoinRoomProcess(uuidValue)case pojo.RefreshScoreType://什么是否进行分数更新,前端判断 type:RefreshScoreType, data:step、step、score//当用户的行为触发前端游戏机制的更新时,前端调用此接口,后端进行分数的转发 不需要做业务处理,直接转发即可fmt.Println("游戏交换中数据", client)client.RefreshScoreProcess(requestPkg)case pojo.DiscontinueQuitType:client.DiscontinueQuitProcess()case pojo.GameOverType://游戏结束类型好像没有太大用,游戏结束的时候的提醒,通过分数更新就可以实现了fmt.Println("GameOverType")case pojo.HeartCheckType://开启一个协程遍历hub中的Client,进行健康检测,生命时间是否会过期,如果过期进行逻辑删除和关闭连接if requestPkg.Data == "PING" {client.HeartCheckProcess()}}

3.观察者模式的引入

观察者模式是使用频率最高的设计模式之一,用于建立对象与对象之间的依赖关系。一个对象发生改变时将自动通知其他对象,其他对象将响应做出反应。在观察者模式中,发生改变的对象称为观察目标,而被通知的对象称为观察者。一个观察目标可以对应多个观察者,而且这些观察者之间可以没有任何相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。

比如在我们的日常生活中,红灯停,绿灯行。在这句话描述的场景中,红绿灯是观察目标,即被观察者;而行人和车辆是观察者;红绿灯即观察目标的状态发生变动的时候,行人和车辆会接收到通知,调整自己的行为。这种建立一个红绿灯对象和多个行人车辆对象之间的依赖关系的模式就是观察者模式。

观察者模式结构中通常包括观察目标和观察者两个继承层次结构,具体结构如下图示意:

Subject是抽象观察目标,我们一般定义为抽象类或者接口,在里面我们规定观察目标应该具有的方法,添加观察者,删除观察者,通知观察者。

ConcreteSubject是具体观察目标,是我们抽象类或者接口的具体实现类,在里面我们定义观察目标方法的具体实现即如何添加观察者、删除观察者、通知观察者。

Observer是抽象观察者,同样地我们一般定义为抽象类或者接口,在里面我们规定观察者应该具有的方法,即观察目标发生变动后的行为,一般我们定义为Update()方法。

ConcreteObserver是具体观察对象,是我们抽象类或者接口的具体实现类,在里面我们定义观察者在观察目标的行为发生变动后,应该执行的具体逻辑代码。

4.观察者模式Demo

Demo的目录结构如下:

subject.go 这里我们定义观察目标接口,里面定义三个方法签名,添加、删除观察者和通知观察者

package subjectimport "demo/TrafficLightsAndPedestrians/observer"type Subject interface {AddPedestriansAndCars(buyer ...observer.Observer)RemovePedestriansAndCars(buyer observer.Observer)NotifyPedestriansAndCars(flag bool)
}

TrafficLights.go 观察目标的具体实现,这里我们模拟红绿灯的情景,因为是具体实现类,直接命名为TrafficLights。在这个类中我们实现了subject接口中定义的所有方法。

package implimport ("demo/TrafficLightsAndPedestrians/observer""fmt"
)type TrafficLights struct {pedestriansAndCars []observer.Observer
}func (p *TrafficLights) AddPedestriansAndCars(buyer ...observer.Observer) {p.buyers = append(p.buyers, buyer...)fmt.Println("可变参数中加入了", p.buyers)
}func (p *TrafficLights) RemovePedestriansAndCars(buyer observer.Observer) {for index, value := range p.buyers {if value == buyer {copy(p.buyers[index:], p.buyers[index+1:])p.buyers = p.buyers[:len(p.buyers)-1]fmt.Println("删除后:", p.buyers)break}}
}
func (p *TrafficLights) NotifyPedestriansAndCars(flag bool) {for _, value := range p.buyers {value.Update(flag)}
}

observer.go 观察者接口,我们定义了一个Update方法,用于更新观察者的行为,当观察目标发生变动的时候,观察者应该执行的行为。

package observertype Observer interface {Update(flag bool)
}

PedestriansAndCars.go 观察者具体方法,由于这里我们模拟的交通信号灯的情景,所以这里观察者的具体实现类直接命名为PedestriansAndCars。这里的Update方法我们实现了当红绿灯发生变动时,行人和车辆应该执行的具体行为,这里我们为了模拟情况,简单地进行打印输出操作。

package implimport "fmt"type PedestriansAndCars struct {Name string
}func (b *PedestriansAndCars) Update(flag bool) {if flag {fmt.Println("绿灯亮", b.Name, "可以走了")} else {fmt.Println("红灯亮", b.Name, "请站在原地等待")}
}

main.go 主函数的场景,在这里我们创建trafficLights观察目标对象,在观察目标中加入行人和车辆,当trafficLights观察目标发生变动的时候,会通知执行所有的已经添加到观察目标切片中的所有行人和车辆。

package mainimport (impl2 "demo/TrafficLightsAndPedestrians/observer/impl""demo/TrafficLightsAndPedestrians/subject/impl"
)func main() {trafficLights := new(impl.TrafficLights)person01 := &impl2.PedestriansAndCars{Name: "小1"}person02 := &impl2.PedestriansAndCars{Name: "小2"}person03 := &impl2.PedestriansAndCars{Name: "小3"}car01 := &impl2.PedestriansAndCars{Name: "车1"}car02 := &impl2.PedestriansAndCars{Name: "车2"}trafficLights.AddPedestriansAndCars(person01, person02, person03, car01, car02)trafficLights.NotifyPedestriansAndCars(false)
}

这里之所以采用接口调用的方式,是为了方便后期代码功能的扩展,如果我们想要在代码中再次添加一个观察目标,直接定义一个结构体去实现subject接口即可,其余代码不需要进行变动;如果·

5.改造后的新代码

本次改造主要是对websocket长连接进行更改,在原有socket包的基础上添加了subject观察目标包和observer观察者,在观察目标发生变动后,会通知所有的观察者,观察者接收到信息后,会执行对应的方法。

在这里观察目标为客户端不断发送的websocket数据包,观察者是原先switch语句下的各个分支。一旦观察目标接收到websocket数据包,就通知所有的观察者,观察者是否否执行,取决于观察者内部信息类型的判断是否符合传送数据包的类型。

subject.go

package observedimport ("klotski/pojo""klotski/socket/subscriber"
)type Observed interface {AddProcess(process subscriber.Subscriber)RemoveProcess(process subscriber.Subscriber)Notify(client *pojo.Client, request *pojo.RequestPkg)
}

controller.go

package implimport ("klotski/pojo""klotski/socket/subscriber"
)type Controller struct {processes []subscriber.Subscriber
}func (c *Controller) AddProcess(process ...subscriber.Subscriber) {c.processes = append(c.processes, process...)}func (c *Controller) RemoveProcess(process subscriber.Subscriber) {for i, o := range c.processes {if o == process {c.processes = append(c.processes[:i], c.processes[i+1:]...)break}}
}func (c *Controller) Notify(client *pojo.Client, request *pojo.RequestPkg) {for _, observer := range c.processes {observer.Update(client, request)}
}

observer.go

package subscriberimport "klotski/pojo"type Subscriber interface {Update(client *pojo.Client, request *pojo.RequestPkg)
}

process.go

package implimport ("encoding/json""fmt""klotski/pojo"
)// CertificationObserver 用户认证观察者
type CertificationObserver struct{}func (o *CertificationObserver) Update(client *pojo.Client, request *pojo.RequestPkg) {if request.Type == pojo.CertificationType {client.CertificationProcess(*request)}
}// CreateRoomObserver 创建房间观察者
type CreateRoomObserver struct{}func (o *CreateRoomObserver) Update(client *pojo.Client, request *pojo.RequestPkg) {if request.Type == pojo.CreateRoomType {client.CreateRoomProcess()}
}// JoinRoomObserver 加入房间观察者
type JoinRoomObserver struct{}func (o *JoinRoomObserver) Update(client *pojo.Client, request *pojo.RequestPkg) {if request.Type == pojo.JoinRoomType {var data map[string]interface{}if err := json.Unmarshal([]byte(request.Data), &data); err != nil {fmt.Println("解析 JSON 失败:", err)}if uuidValue, ok := data["uuid"].(string); !ok {fmt.Println("uuid 字段不存在或不是字符串类型")return} else {client.JoinRoomProcess(uuidValue)}}
}// RefreshScoreObserver 刷新游戏分数观察者
type RefreshScoreObserver struct{}func (o *RefreshScoreObserver) Update(client *pojo.Client, request *pojo.RequestPkg) {if request.Type == pojo.RefreshScoreType {client.RefreshScoreProcess(*request)}
}// DiscontinueQuitObserver 主动断开连接观察者
type DiscontinueQuitObserver struct{}func (o *DiscontinueQuitObserver) Update(client *pojo.Client, request *pojo.RequestPkg) {if request.Type == pojo.DiscontinueQuitType {client.DiscontinueQuitProcess()}
}// GameOverObserver 游戏结束观察者
type GameOverObserver struct{}func (o *GameOverObserver) Update(client *pojo.Client, request *pojo.RequestPkg) {if pojo.GameOverType == request.Type {fmt.Println("GameOverType")}
}// HeartCheckObserver 健康检测观察者
type HeartCheckObserver struct{}func (o *HeartCheckObserver) Update(client *pojo.Client, request *pojo.RequestPkg) {if pojo.HeartCheckType == request.Type {if request.Data == "PING" {client.HeartCheckProcess()}}
}

6.总结

1.观察者模式是一种使用频率非常高的设计模式,无论是移动应用、Web应用或者桌面应用,观察者模式几乎无处不在。它为实现对象之间的联动提供了一套完整的解决方案,凡是涉及一对一或者一对多的对象交互场景都可以使用观察者模式。

2.要学会及时发现自己代码中的问题,并不是代码能够运行起来就可以了,而是要不断进行改进,追求优雅和简洁。

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

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

相关文章

SpingBoot集成Rabbitmq及Docker部署

文章目录 介绍RabbitMQ的特点Rabbitmq术语消息发布接收流程 Docker部署管理界面说明Overview: 这个页面显示了RabbitMQ服务器的一般信息,例如集群节点的名字、状态、运行时间等。Connections: 在这里,可以查看、管理和关闭当前所有的TCP连接。Channels: …

Unity Live Capture 中实现面部捕捉同步模型动画

Unity Face Capture 是一个强大的工具,可以帮助你快速轻松地将真实人脸表情捕捉到数字模型中。在本文中,我们将介绍如何在 Unity Face Capture 中实现面部捕捉同步模型动画。 安装 |实时捕获 |4.0.0 (unity3d.com) 安装软件插件 安装 Live Capture 软件…

Linux_网络项目_WEB服务器 处理服务器写入失败后sigpipe信号导致服务器崩溃退出问题,引入线程池缓解大量请求,服务器组件化重构,在线计算机业务测试

文章目录 1. 处理服务器写入管道出错2. 引入线程池缓解大量请求导致服务器崩溃设计线程任务类单例线程池组件设计 3.代码位置4. 在线计算机业务运行截图 1. 处理服务器写入管道出错 经过测试,服务器在读取报文时如果出错可以选择直接关闭这个TCP里链接来节省资源。…

比特币,区块链及相关概念简介(三)

目录 什么是区块链区块链关键特点区块链存在哪里区块链相关的工作主要是做什么呢可以有多个区块链吗区块链网络节点区块链网络有延迟吗区块链和Rust区块链新技术区块链相关网站 以下内容结合了chatgpt 3.5以及网络文章。 用于学习记录。 简介: 什么是区块链&#xf…

【MySQL高级篇】08-事务篇

第13章:事务基础知识 #09-事务的基础知识#1.事务的完成过程 #步骤1:开启事务 #步骤2:一系列的DML操作 #.... #步骤3:事务结束的状态:提交的状态(COMMIT) 、 中止的状态(ROLLBACK)#2. 显式事务#2.1 如何开启? 使用关键…

项目分享--NO.1

搭建高可用的web集群.部署网站 包含数据库,ceph/nfs,haproxy,keepalived,ansible部署 1,配置ansible管理环境 创建工作目录,编写ansible配置文件,和主机清单文件,yum配置文件 将yum文件到控制机上,然后用模块上传到被管理机器上 #vim 01-upload-repo.yml --- - name: confi…

柚见十三期(优化)

前端优化 加载匹配功能与加载骨架特效 骨架屏 : vant-skeleton index.vue中 /** * 加载数据 */ const loadData async () > { let userListData; loading.value true; //心动模式 if (isMatchMode.value){ const num 10;//推荐人数 userListData await myA…

基于SpringBoot框架实现的B2B平台的医疗病历交互系统

采用技术 基于SpringBoot框架实现的B2B平台的医疗病历交互系统的设计与实现~ 开发语言:Java 数据库:MySQL 技术:SpringBootMyBatis 工具:IDEA/Ecilpse、Navicat、Maven 页面展示效果 管理员角色 医院管理 医院注册 医院文…

samba服务器的配置

需求:在Linux上搭建一个文件共享服务,创建不同的账号给予不同的权限,在windows可以直接访问该共享目录 介绍 Samba 是一个强大的工具,使得不同操作系统之间可以无缝地共享文件和资源,促进了跨平台环境下的协作和通信…

音频的录制及播放

在终端安装好pip install pyaudio,在pycharm中敲入录音的代码,然后点击运行可以在10s内进行录音,录音后的音频会保存在与录音代码同一路径项目中,然后再新建项目敲入播放的代码,点击运行,会把录入的录音进行…

安装 docker 和 jenkins

安装 docker #安装 软件包 docker yum install -y yum-utils device-mapper-persistent-data lvm2#设置 yum 源 yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-c…

C语言-strerror(打印错误信息)和perror(获得错误信息)

strerror(打印错误信息)和perror(获得错误信息) strerror 语法格式 返回类型是char* 都需要头文件 errno.h 这里是错误码 每一个错误码代表一个错误信息 错误码 对照的错误信息 每一种编译器在写的时候已经规定好了 错误码对…

git push origin master error: src refspec master does not match any

一、报错详情 git push origin master error: src refspec master does not match any error: failed to push some refs to git172.20.1.223:xuxj/vue3-smartgf-admin.git 二、解决办法 1.查看一下所有的分支 git branch -l 2.检查远程仓库的分支名 会发现远程是main&am…

四连杆机构运动学仿真 | 【Matlab源码+理论公式文本】

【程序简介】💻🔍 本程序通过matlab实现了四连杆机构的运动学仿真编程,动态展现了四连杆机构的运动动画,同时给出了角位移、角速度和角加速度的时程曲线,除了程序本身,还提供了机构运动学公式推导文档&…

Unity如何让游戏程序读写资源文件?

前言 在Unity中,分为开发环境和打包后环境。 在开发环境中,你可以直接访问项目的文件系统,包括Assets文件夹中的所有文件。但是在打包后的环境中,你不能直接访问文件系统,因为所有的资源都被打包到了一个或多个数据文件…

【Hadoop大数据技术】——MapReduce分布式计算框架(学习笔记)

📖 前言:MapReduce是Hadoop系统核心组件之一,它是一种可用于大数据并行处理的计算模型、框架和平台,主要解决海量数据的计算问题,是目前分布式计算模型中应用较为广泛的一种。 目录 🕒 1. MapReduce概述&am…

Java后端面试:MySQL面试篇(底层事务、SQL调优)

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习 🌌上期文章:Java后端面试:Redis面试篇(原理场景题) 📚订阅专栏:Java后端面试 希望…

Ubuntu 虚拟机安装

最小化安装后常用工具 sudo apt-get install vim# ifconfig apt install net-tools # nload apt install nload # 很多都要用到 apt install build-essential # 开发相关 apt install gcc gapt install iproute2 ntpdate tcpdump telnet traceroute \ nfs-kernel-server nfs…

Leetcode 第 126 场双周赛 Problem D 求出所有子序列的能量和(Java + 数学 + 01背包变种)

文章目录 题目思路Java 数学 01背包变种第一步:第二步:第三步: 复杂度Code 题目 Problem: 100241. 求出所有子序列的能量和给你一个长度为 n 的整数数组 nums 和一个 正 整数 k 。一个整数数组的 能量 定义为和 等于 k 的子序列的数目。请…

Node.js 自带的 http 模块来实现一个简单的本地服务器

1.创建一个 server.js 文件: const http require(http); const fs require(fs); const path require(path);const server http.createServer((req, res) > {// 获取请求的文件路径const filePath path.join(__dirname, dist, req.url);// 读取文件内容并返…