首页 > 编程语言 >编程练习:编写一个监听者模式类

编程练习:编写一个监听者模式类

时间:2025-01-18 20:44:35浏览次数:1  
标签:std Observable 练习 handle observable 编程 观察者 event 监听

监听者模式(Observer Pattern)是一种行为设计模式,它定义了对象之间的一对多依赖关系。当一个对象的状态发生变化时,所有依赖于它的对象都会收到通知并自动更新。这种模式非常适合用于事件驱动的系统,例如 GUI 框架、消息队列等。

在本文中,我们将通过编写一个简单的监听者模式类 Observable,来学习如何实现这一设计模式。


1. 监听者模式的核心概念

监听者模式通常包含以下两个核心组件:

  • Subject(主题):维护一个观察者列表,并提供注册、注销和通知观察者的接口。

  • Observer(观察者):定义一个更新接口,用于接收主题的通知。

在我们的实现中,Observable 类充当 Subject 的角色,而观察者是一个 std::function<void()> 回调函数。


2. 实现 Observable 类

以下是 Observable 类的完整实现:

#ifndef __OBSERVABLE_H___
#define __OBSERVABLE_H___

#include <functional>
#include <mutex>
#include <unordered_map>
#include <cassert>

namespace cise {

/**
 * @class Observable
 * @brief 观察者模式中的可观察对象,允许观察者注册、注销和接收通知。
 * 
 * @tparam Event 观察事件的类型,可以是任意可哈希的类型(如枚举、整数、字符串等)。
 */
template<typename Event>
class Observable final {
public:
    using Handle = int;  // 观察者句柄类型

public:
    Observable() : nextHandle_(0) {}

    /**
     * @brief 添加观察者。
     * 
     * @param event 观察事件的键值。
     * @param callback 观察者的回调函数。
     * @return 返回观察者的句柄,用于后续注销。
     */
    Handle addObserver(Event event, std::function<void()> callback) {
        std::lock_guard<std::mutex> lock(mutex_);

        // 检查 Handle 是否溢出
        assert(nextHandle_ < std::numeric_limits<Handle>::max() && "Handle overflow: maximum number of observers reached.");

        auto& handlers = observers_[event];
        handlers[nextHandle_] = std::move(callback);
        return nextHandle_++;
    }

    /**
     * @brief 移除指定事件和句柄的观察者。
     * 
     * @param event 观察事件的键值。
     * @param handle 观察者的句柄。
     */
    void removeObserver(Event event, Handle handle) {
        std::lock_guard<std::mutex> lock(mutex_);
        auto it = observers_.find(event);
        if (it != observers_.end()) {
            it->second.erase(handle);
        }
    }

    /**
     * @brief 移除所有事件下指定句柄的观察者。
     * 
     * @param handle 观察者的句柄。
     */
    void removeObserver(Handle handle) {
        std::lock_guard<std::mutex> lock(mutex_);
        for (auto& [event, handlers] : observers_) {
            handlers.erase(handle);
        }
    }

    /**
     * @brief 通知所有观察指定事件的观察者。
     * 
     * @param event 观察事件的键值。
     * @note 如果某个观察者的回调函数抛出异常,异常会直接传递给调用者。
     */
    void notifyObservers(Event event) {
        std::lock_guard<std::mutex> lock(mutex_);
        auto it = observers_.find(event);
        if (it != observers_.end()) {
            for (auto& [handle, callback] : it->second) {
                callback();  // 异常会直接传递给调用者
            }
        }
    }

private:
    // 使用 std::unordered_map 而不是 std::map,因为90%场景不需要按事件键值有序遍历观察者,有利于提高性能。
    std::unordered_map<Event, std::unordered_map<Handle, std::function<void()>>> observers_;
    Handle nextHandle_;
    std::mutex mutex_;
};

}  // namespace cise

#endif // __OBSERVABLE_H___

 


3. 设计亮点

3.1 线程安全

使用 std::mutex 保护共享资源,确保在多线程环境下安全地添加、移除和通知观察者。

3.2 高性能

使用 std::unordered_map 存储观察者,避免了有序遍历的开销,提高了性能。

3.3 灵活性

支持任意可哈希的事件类型(如枚举、整数、字符串等),适用于多种场景。

3.4 异常处理

将异常直接传递给调用者,遵循“谁调用,谁处理”的原则。


4. 使用示例

以下是一个简单的使用示例:

 
#include <iostream>
#include "Observable.h"

int main() {
    cise::Observable<std::string> observable;

    // 添加观察者
    auto handle1 = observable.addObserver("event1", []() {
        std::cout << "Observer 1: Event 1 triggered!" << std::endl;
    });

    auto handle2 = observable.addObserver("event2", []() {
        std::cout << "Observer 2: Event 2 triggered!" << std::endl;
    });

    // 通知观察者
    observable.notifyObservers("event1");  // 输出: Observer 1: Event 1 triggered!
    observable.notifyObservers("event2");  // 输出: Observer 2: Event 2 triggered!

    // 移除观察者
    observable.removeObserver("event1", handle1);
    observable.notifyObservers("event1");  // 无输出,观察者已移除

    return 0;
}

 


5. 单元测试

为了确保 Observable 类的正确性,我们编写了以下单元测试:

 
#include <gtest/gtest.h>
#include "Observable.h"

using namespace cise;

TEST(ObservableTest, AddAndNotifyObserver) {
    Observable<std::string> observable;

    bool isCalled = false;
    auto handle = observable.addObserver("event1", [&isCalled]() {
        isCalled = true;
    });

    observable.notifyObservers("event1");
    EXPECT_TRUE(isCalled);
}

TEST(ObservableTest, RemoveObserverByEvent) {
    Observable<std::string> observable;

    bool isCalled = false;
    auto handle = observable.addObserver("event1", [&isCalled]() {
        isCalled = true;
    });

    observable.removeObserver("event1", handle);
    observable.notifyObservers("event1");
    EXPECT_FALSE(isCalled);
}

TEST(ObservableTest, NotifyWithException) {
    Observable<std::string> observable;

    observable.addObserver("event1", []() {
        throw std::runtime_error("Callback failed");
    });

    EXPECT_THROW(observable.notifyObservers("event1"), std::runtime_error);
}

Makefile:

# 编译器
CXX = g++

# 编译选项
CXXFLAGS = -std=c++11 -Wall -Wextra -g -pthread

# Google Test 路径(根据你的安装路径修改)
GTEST_DIR = /path/to/gtest

# 目标文件
TARGET = observable_test

# 源文件
SRCS = ObservableTest.cpp Observable.h

# 编译规则
$(TARGET): $(SRCS)
    $(CXX) $(CXXFLAGS) -isystem $(GTEST_DIR)/include -I$(GTEST_DIR) \
    $(GTEST_DIR)/libgtest.a $(GTEST_DIR)/libgtest_main.a \
    $(SRCS) -o $(TARGET)

# 运行测试
test: $(TARGET)
    ./$(TARGET)

# 清理
clean:
    rm -f $(TARGET)

示例目录结构

假设你的项目目录结构如下:

  复制
/project
    ├── Observable.h
    ├── ObservableTest.cpp
    ├── Makefile
    └── /gtest (Google Test 安装路径)

运行示例

  1. 编译

    make
  2. 运行测试

    make test
  3. 清理

    make clean

     


6. 总结

通过本次编程练习,我们实现了一个简单但功能强大的监听者模式类 Observable。它不仅支持多线程环境,还具有高性能和灵活性。希望这篇文章能帮助你更好地理解监听者模式,并在实际项目中应用它。

如果你有任何问题或建议,欢迎在评论区留言!


参考资料

标签:std,Observable,练习,handle,observable,编程,观察者,event,监听
From: https://www.cnblogs.com/Rong-/p/18678830

相关文章

  • 【花雕学编程】Arduino动手做(246)---使用 Web 服务器的 ESP8266 LED 控制
    37款传感器与执行器的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里准备逐一动手尝试系列实验,不管成功(程序走通)与否,都会记录下来——小小的......
  • 集体智慧编程 : 构建智能Web 2.0应用PDF、EPUB免费下载
    适读人群:Web开发者、架构师、应用工程师电子版仅供预览,支持正版,喜欢的请购买正版书籍点击原文去下载书籍信息作者:[美]TobySegaran出版社:电子工业出版社副标题:构建智能Web2.0应用原作名:ProgrammingCollectiveIntelligence:BuildingSmartWeb2.0Applica......
  • KUKA库卡机械手KRC4手持式编程器维修细节
    在深入KUKA机械手KRC4示教器维修知识之前,首先必须对库卡手持编程器的工作原理和基本构成有所了解。该编程器是连接人与机器人之间的桥梁,通过编程指令控制机器人的动作和任务流程。它通常包括显示屏、输入设备、处理单元以及与机器人通信的接口等部件。【常见库卡机器人KRC4示教盒......
  • OSCP靶场练习从零到一之TR0LL: 1
    本系列为OSCP证书学习训练靶场的记录,主要涉及到vulnhub、HTB上面的OSCP靶场,后续慢慢更新1、靶场介绍名称:TR0LL:1下载地址:https://www.vulnhub.com/entry/tr0ll-1,100/2、信息收集nmap扫描ip,发现目标ip192.168.31.131使用nmap详细扫描nmap-sV192.168.31.13......
  • 系统编程(进程通信--消息队列)
    消息队列概念:消息队列就是一个消息的链表,提供了一种由一个进程向另一个进程发送块数据的方法。另外,每一个数据块被看作有一个类型,而接收进程可以独立接收具有不同类型的数据块,在许多方面看来,消息队列类似于有名管道,但是却没有与打开与关闭管道的复杂关联。优点:1.通过发......
  • 系统编程(进程通信--信号进阶)
    常见问题解决vscode远程连接虚拟机上ubuntu系统,在编写代码时用到的Linux系统函数或者某些常量不提醒或者报红色波浪线的问题:信号的屏蔽和解除信号的屏蔽和解除屏蔽函数的基本使用:#include<stdio.h>#include"header.h"voidhandler(intsignum){pri......
  • 系统编程(进程通信--综合练习)
    实现两个没有亲缘关系的进程之间通过共享内存实现一个小文件(小于10K)的数据拷贝。(可申请文件大小的共享内存,一次性写入文件所有内容,读取共享内存的进程访问数据后,进行文件存储)思路要实现两个进程之间通过共享内存进行文件拷贝,可以按照以下步骤进行:创建共享内存:进程A创......
  • 如何解决虚拟主机无法监听非80端口的问题?
    您好,关于您提到的虚拟主机无法监听非80端口的问题,这是一个常见的限制,但我们可以通过一些方法来解决或绕过这个问题。以下是详细的解决方案和建议:理解虚拟主机的默认端口限制:虚拟主机通常只开放了80(HTTP)、443(HTTPS)和21(FTP)等常用端口。这是因为这些端口是Web服务的标准端口,且为......
  • 编程题-生成交替二进制字符串的最小操作数
    题目:给你一个仅由字符'0'和'1'组成的字符串s。一步操作中,你可以将任一'0'变成'1',或者将'1'变成'0'。交替字符串定义为:如果字符串中不存在相邻两个字符相等的情况,那么该字符串就是交替字符串。例如,字符串"010"是交替字符串,而字符串"0100"不是。返回使s......
  • 实现异步编程有哪些方式?推荐用哪种?
    实现异步编程在前端开发中有多种方式,每种方式都有其特定的使用场景和优缺点。以下是一些常见的异步编程实现方式:回调函数(Callback):回调函数是最原始且广泛使用的异步编程方式之一。当一个异步操作完成时,它会调用一个作为参数传递的函数,即回调函数,并将结果作为参数传递给该函......