设计模式 —— 观察者模式

设计模式 —— 观察者模式

  • 什么是观察者模式
      • 观察者模式定义
      • 观察者模式的角色
      • 观察者模式的使用场景
      • 观察者模式的实现
  • 被观察者(Subject)
  • 观察者(Observer)
  • 通知(notify)
  • 更新显示(update)
  • 观察者模式的优缺点
      • 优点
      • 缺点

我们今天来介绍观察者模式

什么是观察者模式

观察者模式定义

观察者模式(Observer Pattern)是一种行为型设计模式,它定义了对象间的一对多依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会自动收到通知并更新。在这种模式中,一个目标对象(被观察对象)管理所有相依于它的观察者对象,并在其状态改变时主动发出通知。观察者模式通常被用来实现事件处理系统.

观察者模式的角色

观察者模式涉及以下几个核心角色:

  1. 主题(Subject):也称为被观察者或可观察者,它是具有状态的对象,并维护着一个观察者列表。主题提供了添加、删除和通知观察者的方法.
  1. 观察者(Observer):观察者是接收主题通知的对象。观察者需要实现一个更新方法,当收到主题的通知时,调用该方法进行更新操作.
  1. 具体主题(Concrete Subject):具体主题是主题的具体实现类。它维护着观察者列表,并在状态发生改变时通知观察者.
  1. 具体观察者(Concrete Observer):具体观察者是观察者的具体实施类。它实现了更新方法,定义了在收到主题通知时需要执行的具体操作.

观察者模式的使用场景

观察者模式适用于以下场景:

  1. 当一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这两者封装在独立的对象中以使它们可以各自独立地改变和复用.
  1. 当对一个对象的改变需要同时改变其他对象,而不知道具体有多少对象需要被改变.
  1. 当一个对象必须通知其他对象,而它又不能假定其他对象是谁.

缺点:

  • 在应用观察者模式时需要考虑一些开发小路问题,程序中包括一个被观察者和多个被观察者,开发和调试比较复杂.

  • 在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:
    // 构造函数,初始化怪物的血量为10
    Monster() : _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:
    // 默认构造函数,设置初始血量为100
    Monster() :_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; // 怪物的气势,默认为0
    std::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)是一种行为设计模式,它定义了对象之间的一对多依赖关系,当一个对象状态改变时,所有依赖于它的对象都会得到通知并自动更新。以下是观察者模式的主要优点和缺点:

优点

  1. 松耦合性(Decoupling):观察者模式通过抽象接口或抽象类定义了观察者和被观察者之间的交互,使得两者之间的依赖关系变得松散。这提高了系统的可维护性和可扩展性,因为修改一个类不会直接影响到其他类。
  1. 灵活性和动态性:可以很容易地在运行时动态添加新的观察者对象或移除现有观察者,而无需修改被观察者的代码,这使得系统非常灵活和易于扩展。
  1. 广播通知:被观察者可以一次性通知所有注册的观察者,减少了代码重复,并且能够确保状态的同步更新。
  1. 模块化:观察者模式促进了软件模块化设计,观察者和被观察者可以独立开发和测试,它们之间的交互通过接口标准化。

缺点

  1. 性能开销:当观察者数量很大时,通知所有观察者可能会引起性能问题,尤其是在每次状态变化都需要通知时。这可能涉及大量的遍历和调用操作。
  1. 过度通知:如果被观察者频繁改变状态,可能会导致不必要的通知,观察者可能接收到很多不必要的更新,增加了处理负担。
  1. 循环依赖和复杂性:如果观察者和被观察者之间形成了复杂的相互依赖关系,可能会导致难以理解和维护的循环引用问题,甚至系统死锁。
  1. 调试困难:由于观察者模式的异步和松耦合特性,有时很难跟踪和调试问题,尤其是当多个观察者同时响应并可能互相影响时。

总的来说,观察者模式适合那些需要维护多个对象间状态同步,且这些对象之间的关系可以抽象为一对多依赖的场景。但在应用时需要权衡其带来的灵活性和可能的性能、维护问题。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

44【Aseprite 作图】樱花丸子——拆解

1 枝干 2 花朵&#xff1a;其实只要形状差不多都行&#xff0c;有三个颜色&#xff0c;中间花蕊颜色深一点&#xff0c;中间花蕊外的颜色偏白&#xff1b;不透明度也可以改一下&#xff0c;就变成不同颜色 3 丸子 最外层的颜色最深&#xff0c;中间稍浅&#xff0c;加一些高光…

Jmeter分布式、测试报告、并发数计算、插件添加方式、常用图表

Jmeter分布式 应用场景 当单个测试机无法模拟用户要求的业务场景时&#xff0c;可以使用多台测试机进行模拟&#xff0c;就是Jmeter的分布 式测试。 Jmeter分布式执行原理 Jmeter分布测试时&#xff0c;选择其中一台作为控制机&#xff08;Controller&#xff09;&#xff0c…

第十二届蓝桥杯单片机国赛练习代码

文章目录 前言一、问题重述二、主函数总结 前言 第十五蓝桥杯国赛落幕已有十天&#xff0c;是时候总结一下&#xff0c;这个专栏也将结束。虽然并没有取得预期的结果&#xff0c;但故事结尾并不总是美满的。下面是赛前练习的第十二届国赛的代码。 一、问题重述 二、主函数 完整…

JavaScript前端技术入门教程

引言 在前端开发的广阔天地中&#xff0c;JavaScript无疑是最耀眼的一颗明星。它赋予了网页动态交互的能力&#xff0c;让网页从静态的文本和图片展示&#xff0c;进化为可以与用户进行实时交互的丰富应用。本文将带您走进JavaScript的世界&#xff0c;为您提供一个入门级的教…

文件的基础必备知识(初学者入门)

1. 为什么使用文件 2. 什么是文件 3. 二进制文件和文本文件 4. 文件的打开和关闭 1.为什么使用文件 我们写的程序数据是存储在电脑内存中&#xff0c;如果程序退出&#xff0c;内存回收&#xff0c;数据就丢失&#xff0c;等程序再次运行时&#xff0c;上次的数据已经消失。面…

C++~~期末复习题目讲解---lijiajia版本

目录 1.类和对象 &#xff08;3&#xff09;创建对象的个数 &#xff08;3&#xff09;全局变量&#xff0c;局部变量 &#xff08;4&#xff09;构造函数的执行次数 &#xff08;5&#xff09;静态动态析构和构造顺序 &#xff08;6&#xff09;初始化顺序和声明顺序 &a…

MySQL数据库的基础:逻辑集合数据库与表的基础操作

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人能接…

简单聊聊大数据解决方案

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…

Spring5

文章目录 1. Spring 是什么&#xff1f;2. IoC3. Spring Demo4. IoC 创建对象的方式 / DI 方式注入的默认参数在哪里设定? 5. Spring 配置tx:annotation-driven 用于启用基于注解的事务管理 6. Bean的作用域7. 在Spring中有三种自动装配的方式1. 在xml中显式的配置2. 在java中…

node的安装

node是前端开发环境&#xff0c;所以运行前端程序需要安装和配置node 1. 下载安装node 去node官网选择你需要的版本进行下载 Node.js — Download Node.js (nodejs.org) ​ 下载到本地后一路点击next傻瓜式安装&#xff0c;安装成功后测试是否安装成功 node -v 显示node版…

电路防护-贴片陶瓷气体放电管

贴片陶瓷气体放电管 GDT工作原理GDT主要特性参数典型电路压敏电阻与 TVS 管的区别 GDT工作原理 陶瓷气体放电管是一种电子器件&#xff0c;其工作原理基于气体放电现象。这种管子的内部填充了一种特定的气体&#xff0c;通常是氖气或氩气。当管子两端施加足够的电压时&#xf…

刚刚❗️德勤2025校招暑期实习测评笔试SHL测评题库已发(答案)

&#x1f4e3;德勤 2024暑期实习测评已发&#xff0c;正在申请的小伙伴看过来哦&#x1f440; ㊙️本次暑期实习优先考虑2025年本科及以上学历的毕业生&#xff0c;此次只有“审计及鉴定”“税务与商务咨询”两个部门开放了岗位~ ⚠️测评注意事项&#xff1a; &#x1f44…

USB转I2C转SPI芯片CH341

CH340与CH341区别 CH340主要用于将USB转换为各种串口&#xff0c;CH340H和CH340S可以实现USB转并口。 CH341和340的不同之处在于CH341提供I2C和SPI接口&#xff0c;方便连接到I2C或SPI总线操作相关的器件。 CH341主要有6种封装。见表1. CH341T SSOP-20封装和丝印 USB 总线转接…

大模型基础——从零实现一个Transformer(2)

大模型基础——从零实现一个Transformer(1) 一、引言 上一章主要实现了一下Transformer里面的BPE算法和 Embedding模块定义 本章主要讲一下 Transformer里面的位置编码以及多头注意力 二、位置编码 2.1正弦位置编码(Sinusoidal Position Encoding) 其中&#xff1a; pos&…

【JVM】从编译后的指令集来再次理解++i和i++的执行顺序

JVM为什么要选用基于栈的指令集架构 与基于寄存器的指令集架构相比&#xff0c;基于栈的指令集架构不依赖于硬件&#xff0c;因此可移植性更好&#xff0c;跨平台性更好因为栈结构的特性&#xff0c;永远都是先处理栈顶的第一条指令&#xff0c;因此大部分指令都是零地址指令&…

SpringMVC[从零开始]

SpringMVC SpringMVC简介 1.1什么是MVC MVC是一种软件架构的思想&#xff0c;将软件按照模型、视图、控制器来划分 M:Model&#xff0c;模型层&#xff0c;指工程中的JavaBean&#xff0c;作用是处理数据 JavaBean分为两类&#xff1a; 一类称为实体类Bean&#xff1a;专…

对猫毛过敏?怎么有效的缓解过敏症状,宠物空气净化器有用吗?

猫过敏是一种常见的过敏反应&#xff0c;由猫的皮屑、唾液或尿液中的蛋白质引起。这些蛋白质被称为过敏原&#xff0c;它们可以通过空气传播&#xff0c;被人体吸入后&#xff0c;会触发免疫系统的过度反应。猫过敏是宠物过敏中最常见的类型之一&#xff0c;对许多人来说&#…

【Java】static 修饰变量

static 一种java内置关键字&#xff0c;静态关键字&#xff0c;可以修饰成员变量、成员方法。 static 成员变量 1.static 成员变量2.类变量图解3.类变量的访问4.类变量的内存原理5.类变量的应用 1.static 成员变量 成员变量按照有无static修饰&#xff0c;可以分为 类变量…

Python学习打卡:day02

day2 笔记来源于&#xff1a;黑马程序员python教程&#xff0c;8天python从入门到精通&#xff0c;学python看这套就够了 8、字符串的三种定义方式 字符串在Python中有多种定义形式 单引号定义法&#xff1a; name 黑马程序员双引号定义法&#xff1a; name "黑马程序…

如何为色盲适配图形用户界面

首发日期 2024-05-25, 以下为原文内容: 答案很简单: 把彩色去掉, 测试. 色盲, 正式名称 色觉异常. 众所周知, 色盲分不清颜色. 如果用户界面设计的不合理, 比如不同项目只使用颜色区分, 而没有形状区分, 那么色盲使用起来就会非常难受, 甚至无法使用. 色盲中最严重的情况称为…