设计模式 —— 观察者模式
- 什么是观察者模式
- 观察者模式定义
- 观察者模式的角色
- 观察者模式的使用场景
- 观察者模式的实现
- 被观察者(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:
// 构造函数,初始化怪物的血量为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)是一种行为设计模式,它定义了对象之间的一对多依赖关系,当一个对象状态改变时,所有依赖于它的对象都会得到通知并自动更新。以下是观察者模式的主要优点和缺点:
优点
- 松耦合性(Decoupling):观察者模式通过抽象接口或抽象类定义了观察者和被观察者之间的交互,使得两者之间的依赖关系变得松散。这提高了系统的可维护性和可扩展性,因为修改一个类不会直接影响到其他类。
- 灵活性和动态性:可以很容易地在运行时动态添加新的观察者对象或移除现有观察者,而无需修改被观察者的代码,这使得系统非常灵活和易于扩展。
- 广播通知:被观察者可以一次性通知所有注册的观察者,减少了代码重复,并且能够确保状态的同步更新。
- 模块化:观察者模式促进了软件模块化设计,观察者和被观察者可以独立开发和测试,它们之间的交互通过接口标准化。
缺点
- 性能开销:当观察者数量很大时,通知所有观察者可能会引起性能问题,尤其是在每次状态变化都需要通知时。这可能涉及大量的遍历和调用操作。
- 过度通知:如果被观察者频繁改变状态,可能会导致不必要的通知,观察者可能接收到很多不必要的更新,增加了处理负担。
- 循环依赖和复杂性:如果观察者和被观察者之间形成了复杂的相互依赖关系,可能会导致难以理解和维护的循环引用问题,甚至系统死锁。
- 调试困难:由于观察者模式的异步和松耦合特性,有时很难跟踪和调试问题,尤其是当多个观察者同时响应并可能互相影响时。
总的来说,观察者模式适合那些需要维护多个对象间状态同步,且这些对象之间的关系可以抽象为一对多依赖的场景。但在应用时需要权衡其带来的灵活性和可能的性能、维护问题。