设计模式 —— 观察者模式
设计模式 —— 观察者模式
- 什么是观察者模式
- 观察者模式定义
- 观察者模式的角色
- 观察者模式的使用场景
- 观察者模式的实现
- 被观察者(Subject)
- 观察者(Observer)
- 通知(notify)
- 更新显示(update)
- 观察者模式的优缺点
- 优点
- 缺点
我们今天来介绍观察者模式:
什么是观察者模式
观察者模式定义
观察者模式(Observer Pattern)是一种行为型设计模式,它定义了对象间的一对多依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会自动收到通知并更新。在这种模式中,一个目标对象(被观察对象)管理所有相依于它的观察者对象,并在其状态改变时主动发出通知。观察者模式通常被用来实现事件处理系统.
观察者模式的角色
观察者模式涉及以下几个核心角色:
- 主题(Subject):也称为被观察者或可观察者,它是具有状态的对象,并维护着一个观察者列表。主题提供了添加、删除和通知观察者的方法.
- 观察者(Observer):观察者是接收主题通知的对象。观察者需要实现一个更新方法,当收到主题的通知时,调用该方法进行更新操作.
- 具体主题(Concrete Subject):具体主题是主题的具体实现类。它维护着观察者列表,并在状态发生改变时通知观察者.
- 具体观察者(Concrete Observer):具体观察者是观察者的具体实施类。它实现了更新方法,定义了在收到主题通知时需要执行的具体操作.
观察者模式的使用场景
观察者模式适用于以下场景:
- 当一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这两者封装在独立的对象中以使它们可以各自独立地改变和复用.
- 当对一个对象的改变需要同时改变其他对象,而不知道具体有多少对象需要被改变.
- 当一个对象必须通知其他对象,而它又不能假定其他对象是谁.
缺点:
-
在应用观察者模式时需要考虑一些开发小路问题,程序中包括一个被观察者和多个被观察者,开发和调试比较复杂.
-
在Java中的消息的通知默认是顺序执行的,一个观察者的卡顿会影响整体的执行效率。在这种情况下,一般考虑采用异步的方式.
观察者模式的实现
实现观察者模式有多种形式,一种直观的方式是使用“注册—通知—撤销注册”的形式。观察者将自己注册到被观察对象中,被观察对象将观察者存放在一个容器里。当被观察对象发生了某种变化,它从容器中得到所有注册过的观察者,将变化通知观察者。观察者告诉被观察对象要撤销观察,被观察对象从容器中将观察者去除.
我们举个例子:玩家攻击怪兽掉血显示血量,增加气势
被观察者(Subject)
怪兽(或其血量管理类)扮演“被观察者(Subject)”角色,负责维持血量状态并管理观察者列表。
// 定义被观察者接口,任何能够被观察的状态持有者(如怪物)需要实现这个接口
class Subject {
public:// 虚析构函数,确保通过基类指针删除子类对象时能正确调用子类析构函数virtual ~Subject() {}// attach: 注册观察者到被观察者,使其可以接收状态变更通知virtual void attach(Observer* observer) = 0;// detach: 解注册观察者,不再接收状态变更通知virtual void detach(Observer* observer) = 0;// notify: 通知所有观察者血量变化virtual void notify(int health) = 0;// notifyMoraleChange: 通知所有观察者气势变化virtual void notifyMoraleChange() = 0;
};// 怪物类,继承自被观察者接口,表示它是可被观察的状态持有者
class Monster : public Subject {
public:// 构造函数,初始化怪物的血量为10Monster() : _health(100), _morel(0) {}// 析构函数,清理资源,虽然当前版本未直接管理额外资源,但保持以备未来扩展~Monster() {}private:// _health: 怪物的当前血量int _health;// _morel: 怪物的当前气势值int _morale;// _observers: 存储所有观察怪物状态的观察者指针集合std::vector<Observer*> _observers;
};
观察者(Observer)
UI显示组件作为“观察者(Observer)”,订阅怪兽的血量变化。
// 定义观察者接口,任何想要监听怪物状态变化的实体都需要实现这个接口
class Monster : public Subject
public:// updateHealthy: 当怪物血量发生变化时,观察者会被通知virtual void updateHealthy(int health) = 0;// updateMorale: 当怪物气势发生变化时,观察者会被通知virtual void updateMorale(int morale) = 0;
};
通知(notify)
当玩家的攻击导致怪兽血量减少时,怪兽对象通知所有观察者(即UI组件)。
// 通知所有观察者血量变化,调用观察者的 updateHealthy 方法。void notifyHeath(int health) override {for(auto & observer: _observers) {observer->updateHealthy(health); // 应修正参数传递,确保观察者获得实际的血量值。}}// 通知所有观察者气势变化,调用观察者的 updateMorel 方法。void notifyMoraleChange() override {for(auto & observer: _observers) {observer->updateMorel(_morale); // 同样,传递当前气势值给观察者。}}void takeDamage(int damage){_health -= damage;if (_health < 0) _health = 0;_morale += 20; // 气势增加notifyHeath(_health); // 通知血量变化notifyMoraleChange(); // 新增:通知气势变化}
更新显示(update)
观察者收到通知后,各自更新显示的血量信息。
//属性条
class Classbuff : public Observer
{
public:void updateHealthy(int health) override{std::cout << "Health Bar Update: Current HP is " << health << std::endl;}void updateMorel(int morale) override{std::cout << "Morale Update: Current Morale is " << morale << std::endl;}
};
完整代码如下:
// 使用#pragma once防止头文件重复包含
#pragma once// 引入所需的标准库
#include<iostream>
#include<vector>// **观察者类定义**
// 观察者接口,任何观察怪物状态的类需要实现这两个更新方法
class Observer {
public:// 纯虚函数,更新血量virtual void updateHealthy(int health) = 0;// 纯虚函数,更新气势virtual void updateMorel(int morale) = 0;
};// **被观察者接口定义**
// 定义被观察者需要实现的接口,用于管理观察者列表及通知状态变化
class Subject {
public:// 虚析构函数,确保通过基类指针可以安全删除子类对象virtual ~Subject() {}// 接口方法,注册观察者virtual void attach(Observer* observer) = 0;// 接口方法,注销观察者virtual void detach(Observer* observer) = 0;// 接口方法,通知所有观察者血量变化virtual void notifyHeath() = 0;// 接口方法,通知所有观察者气势变化virtual void notifyMoraleChange() = 0;
};// **Monster类定义**
// 继承自Subject,代表被观察者(怪物)
class Monster : public Subject {
public:// 默认构造函数,设置初始血量为100Monster() :_health(100) {}// 构造函数,允许设置初始血量Monster(int health) :_health(health) {}// 析构函数,删除所有观察者对象(假设Monster拥有观察者对象所有权)~Monster() {for(auto observer : _observers) {delete observer;}}// 实现attach方法,添加观察者到列表void attach(Observer* observer) override {_observers.push_back(observer);}// 实现detach方法,从列表中移除指定观察者void detach(Observer* observer) override {for(auto it = _observers.begin(); it != _observers.end(); ) {if(*it == observer) {it = _observers.erase(it);} else {++it;}}}// 实现notifyHeath,通知观察者血量变化void notifyHeath() override {for(auto observer: _observers) {observer->updateHealthy(_health);}}// 实现notifyMoraleChange,通知观察者气势变化void notifyMoraleChange() override {for(auto observer: _observers) {observer->updateMorel(_morale);}}// 减少怪物血量并增加气势,同时通知观察者void takeDamage(int damage) {_health -= damage;if (_health < 0) _health = 0;_morale += 20; // 气势增加notifyHeath(); // 通知血量变化notifyMoraleChange(); // 通知气势变化}private:int _health; // 怪物的血量int _morale; // 怪物的气势,默认为0std::vector<Observer*> _observers; // 存储观察者指针的向量
};// **Classbuff类定义**
// 实现Observer接口,代表一个具体的观察者(如血量条)
class Classbuff : public Observer {
public:// 实现更新血量显示void updateHealthy(int health) override {std::cout << "Health Bar Update: Current HP is " << health << std::endl;}// 实现更新气势显示void updateMorel(int morale) override {std::cout << "Morale Update: Current Morale is " << morale << std::endl;}
};
这段代码通过观察者模式展示了如何设计一个怪物类(Monster
)和一个观察者类(Classbuff
)。怪物类负责维护血量和气势状态,并在状态变化时通知所有注册的观察者。Classbuff
类作为观察者,负责接收通知并打印出怪物的血量或气势变化信息。
我们来试试:
#define _CRT_SECURE_NO_WARNINGS 1
//#include "Monster.h"#include "monster_2.h"int main()
{Monster monster(100); // 创建一个初始血量为100的怪兽Classbuff* healthBar = new Classbuff(); // 使用指针以匹配detach操作// 怪兽注册生命条观察者monster.attach(healthBar);// 玩家攻击,造成20点伤害monster.takeDamage(20);// 假设需要在某个时刻移除观察者// monster.detach(healthBar);return 0;}
观察者模式的优缺点
观察者模式(Observer Pattern)是一种行为设计模式,它定义了对象之间的一对多依赖关系,当一个对象状态改变时,所有依赖于它的对象都会得到通知并自动更新。以下是观察者模式的主要优点和缺点:
优点
- 松耦合性(Decoupling):观察者模式通过抽象接口或抽象类定义了观察者和被观察者之间的交互,使得两者之间的依赖关系变得松散。这提高了系统的可维护性和可扩展性,因为修改一个类不会直接影响到其他类。
- 灵活性和动态性:可以很容易地在运行时动态添加新的观察者对象或移除现有观察者,而无需修改被观察者的代码,这使得系统非常灵活和易于扩展。
- 广播通知:被观察者可以一次性通知所有注册的观察者,减少了代码重复,并且能够确保状态的同步更新。
- 模块化:观察者模式促进了软件模块化设计,观察者和被观察者可以独立开发和测试,它们之间的交互通过接口标准化。
缺点
- 性能开销:当观察者数量很大时,通知所有观察者可能会引起性能问题,尤其是在每次状态变化都需要通知时。这可能涉及大量的遍历和调用操作。
- 过度通知:如果被观察者频繁改变状态,可能会导致不必要的通知,观察者可能接收到很多不必要的更新,增加了处理负担。
- 循环依赖和复杂性:如果观察者和被观察者之间形成了复杂的相互依赖关系,可能会导致难以理解和维护的循环引用问题,甚至系统死锁。
- 调试困难:由于观察者模式的异步和松耦合特性,有时很难跟踪和调试问题,尤其是当多个观察者同时响应并可能互相影响时。
总的来说,观察者模式适合那些需要维护多个对象间状态同步,且这些对象之间的关系可以抽象为一对多依赖的场景。但在应用时需要权衡其带来的灵活性和可能的性能、维护问题。
相关文章:

设计模式 —— 观察者模式
设计模式 —— 观察者模式 什么是观察者模式观察者模式定义观察者模式的角色观察者模式的使用场景观察者模式的实现 被观察者(Subject)观察者(Observer)通知(notify)更新显示(update)…...

光纤跳线(又称光纤连接器)的种类
光纤跳线(又称光纤连接器),也就是接入光模块的光纤接头,也有好多种,且相互之间不可以互用。SFP模块接LC光纤连接器,而GBIC接的是SC光纤连接器。下面对网络工程中几种常用的光纤连接器进行详细的说明&#x…...
探索Ubuntu:从入门到精通
目录 一、什么是Ubuntu? 1.1 Ubuntu的定义和背景 1.2 Ubuntu的特点 二、安装Ubuntu 2.1 下载Ubuntu安装镜像 2.2 制作启动盘 2.3 安装Ubuntu 三、初步设置和基本操作 3.1 系统更新 3.2 安装必要软件 3.3 设置和管理用户账户 四、文件和目录管理 4.1 文件管理器 …...

SpringMVC-基础架构
一、什么是MVC 二、什么是SpringMVC 三、SpringMVC的特点 四、配置SpringMVC 简单流程: 总体框架 1.创建pom.xml依赖 <!--打包方式--><packaging>war</packaging><!--依赖--><dependencies><dependency><groupId>org.s…...
《Windows API每日一练》4.1 GDI绘图
本节必须掌握的知识点: GDI原理 GDI函数调用 GDI基本图形 4.1.1 GDI原理 GDI,全称是Graphics Device Interface(图形设备接口),是微软Windows操作系统中提供的一套用于渲染图形和格式化文本的API(应用程序…...

SQL Server 安装后,服务器再改名,造成名称不一致,查询并修改数据库服务器真实名称
SELECT SERVERNAME -- 1.查询旧服务器名称 SELECT serverproperty(servername) AS new --2.查询新服务器名称 -- 3.更新服务器名称 IF SERVERPROPERTY(servername) <> 新服务器名称替换 BEGIN DECLARE server_name NVARCHAR(128) SET server_name 新服务器…...

单例模式、工厂模式 c++关键字 static
static 关键字的作用: 主要作用在于 控制变量或函数的作用域、生命周期以及它们如何被不同部分的程序访问,从而帮助程序员管理内存、避免命名冲突,并实现特定的设计模式(如单例模式)。 1. 静态局部变量:当…...

基于文本和图片输入的3D数字人化身生成技术解析
随着虚拟现实、增强现实和元宇宙等技术的飞速发展,对高度逼真且具有表现力的3D数字人化身的需求日益增长。传统的3D数字人生成方法往往需要依赖大量的3D数据集,这不仅增加了数据收集和处理的成本,还限制了生成的多样性和灵活性。为了克服这些挑战,我们提出了一种基于文本提…...

C语言 | Leetcode C语言题解之第150题逆波兰表达式求值
题目: 题解: int evalRPN(char** tokens, int tokensSize) {int n tokensSize;int stk[(n 1) / 2];memset(stk, 0, sizeof(stk));int index -1;for (int i 0; i < n; i) {char* token tokens[i];if (strlen(token) > 1 || isdigit(token[0])…...
API安全性的重要性及实施策略
在当今日益互联的世界中,API(应用程序编程接口)成为连接不同软件系统的关键桥梁。随着API的使用越来越广泛,其安全性问题也日益凸显。一个不安全的API可能会使企业数据和用户信息面临严重的风险。因此,确保API的安全性…...

现在Java行情不好可以转.net吗?
转向.NET开发可能是一个选择,但要注意以下几点。我这里有一套编程入门教程,不仅包含了详细的视频 讲解,项目实战。如果你渴望学习编程,不妨点个关注,给个评论222,私信22,我在后台发给你。 技术转…...

大文件word生成的处理与解决策略
前言 对于简单word文档的生成导出,java已经有着很多技术来进行处理,在有着相对固定的格式样板下,采用word模板导出相对会是比较好的选择。但是当数据量且包含大量图片后,采用模板导出就显得无力了,模板的缺点是无法应…...

unity3d:GameFramework+xLua+Protobuf+lua-protobuf,与服务器交互收发协议
概述 1.cs收发协议,通过protobuf序列化 2.lua收发协议,通过lua-protobuf序列化 一条协议字节流组成 C#协议基类 CSPacketBase,SCPacketBaseC#用协议基类 proto生成的CS类,基于这两个基类。分别为CSPacketBase是客户端发送至服…...

二刷算法训练营Day30 | 回溯算法(6/6)
目录 详细布置: 1. 回溯总结 2. 332. 重新安排行程 3. 51. N 皇后 4. 37. 解数独 详细布置: 1. 回溯总结 回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起&#x…...

【车载AI音视频电脑】200万像素迷你一体机
产品主要特点: -设备安装方便简洁,可通过3M胶直接将设备粘 贴到车前挡风玻璃上 -支持IE预览,手机,PAD实时预览, 支持电脑客 户端实时预览功能 -内置2路模拟高清, 每路均可达到200万像素。另 外可扩充2路1080P模拟…...

齐普夫定律在循环神经网络中的语言模型的应用
目录 齐普夫定律解释公式解释图与公式的关系代码与图的分析结论 使用对数表达方式的原因1. 线性化非线性关系2. 方便数据可视化和分析3. 降低数值范围4. 方便参数估计公式详细解释结论 来自:https://zh-v2.d2l.ai/chapter_recurrent-neural-networks/language-model…...
如何在Android Studio上发布Flutter应用
发布Flutter应用到Android平台是一个多步骤的过程,涉及配置应用、生成签名密钥、配置Gradle文件、构建发布版本APK等步骤。本文将详细介绍这些步骤,帮助你顺利发布Flutter应用。 1. 准备你的应用 在发布之前,确保你的应用在开发环境中运行良…...
C++ 字符串处理4-根据指定的分隔符将字符串分割为多个子串根据指定的分隔符将多个子串连接成一个字符串
1. 关键词 C 字符串处理 分割字符串 连接字符串 跨平台 2. strutil.h #pragma once#include <string> #include <vector>namespace cutl {/*** brief The type of vector strings used in this library.**/using strvec std::vector<std::string>;/*** b…...

微信小程序请求request封装
公共基础路径封装 // config.js module.exports {// 测试BASE_URL: https://cloud.chejj.cn,// 正式// BASE_URL: https://cloud.mycjj.com };请求封装 // request.js import config from ../config/baseUrl// 请求未返回时的loading const showLoading () > wx.showLoadi…...
Web前端不挂科:深入探索与实战指南
Web前端不挂科:深入探索与实战指南 在数字化时代的浪潮中,Web前端开发已成为一项炙手可热的技能。然而,对于许多初学者来说,如何避免在Web前端课程中挂科却成为了一道难题。本文将从四个方面、五个方面、六个方面和七个方面&…...

Linux应用开发之网络套接字编程(实例篇)
服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻
在如今就业市场竞争日益激烈的背景下,越来越多的求职者将目光投向了日本及中日双语岗位。但是,一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧?面对生疏的日语交流环境,即便提前恶补了…...
rknn优化教程(二)
文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK,开始写第二篇的内容了。这篇博客主要能写一下: 如何给一些三方库按照xmake方式进行封装,供调用如何按…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...
反射获取方法和属性
Java反射获取方法 在Java中,反射(Reflection)是一种强大的机制,允许程序在运行时访问和操作类的内部属性和方法。通过反射,可以动态地创建对象、调用方法、改变属性值,这在很多Java框架中如Spring和Hiberna…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...

20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...