You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust中带动态载荷的扩展观察者模式实现方案问询

问题描述

我基于Refactoring Guru的示例实现了观察者模式,但需要扩展功能,让notify能发送事件专属的载荷。直接将Event枚举改为带数据的形式会导致订阅问题——因为HashMap的键是带具体数据的枚举值,订阅者无法监听整个事件类型(比如所有ElementACreated事件,而非某个特定实例)。

现有基础实现:

#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub enum Event {
    ElementACreated,
    ElementADeleted,
    ElementBCreated,
    ElementBDeleted,
}

pub type Subscriber = fn(event: Event);

#[derive(Default)]
pub struct Publisher {
    events: HashMap<Event, Vec<Subscriber>>,
}

impl Publisher {
    pub fn subscribe(&mut self, event_type: Event, listener: Subscriber) {
        self.events.entry(event_type.clone()).or_default();
        self.events.get_mut(&event_type).unwrap().push(listener);

        debug!("Subscribed to event: {:?}", event_type);
    }

    pub fn unsubscribe(&mut self, event_type: Event, listener: Subscriber) {
        self.events.get_mut(&event_type).unwrap().retain(|&x| x != listener);

        debug!("Unsubscribed from event: {:?}", event_type);
    }

    pub(crate) fn notify(&self, event_type: Event) {
        let listeners = self.events.get(&event_type).unwrap();
        for listener in listeners {
            listener(event_type.clone());
        }

        debug!("Notified {} listeners about event: {:?}", listeners.len(), event_type);
    }
}

预期效果是notify能给不同事件发送对应载荷,比如ElementACreated发送ElementA实例,ElementADeleted发送ElementAId等。

解决方案

核心思路是将**事件类型(用于订阅)和事件实例(带载荷,用于通知)**分离,同时保证类型安全。

1. 定义事件类型与事件实例

首先定义不带数据的EventType作为订阅的键,再定义带对应载荷的Event作为通知载体:

// 假设已定义以下业务结构体
#[derive(Debug, Clone)]
pub struct ElementA { /* 业务字段 */ }
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ElementAId(u64);
#[derive(Debug, Clone)]
pub struct ElementB { /* 业务字段 */ }
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ElementBId(u64);

// 用于订阅的事件类型(无载荷)
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub enum EventType {
    ElementACreated,
    ElementADeleted,
    ElementBCreated,
    ElementBDeleted,
}

// 用于通知的事件实例(带专属载荷)
#[derive(Debug, Clone)]
pub enum Event {
    ElementACreated(ElementA),
    ElementADeleted(ElementAId),
    ElementBCreated(ElementB),
    ElementBDeleted(ElementBId),
}

// 关联事件实例与事件类型
impl Event {
    pub fn event_type(&self) -> EventType {
        match self {
            Event::ElementACreated(_) => EventType::ElementACreated,
            Event::ElementADeleted(_) => EventType::ElementADeleted,
            Event::ElementBCreated(_) => EventType::ElementBCreated,
            Event::ElementBDeleted(_) => EventType::ElementBDeleted,
        }
    }
}

2. 重构Publisher

使用EventType作为HashMap的键,订阅者接收&Event以获取载荷,同时优化取消订阅的可靠性(通过订阅ID):

use std::collections::HashMap;
use log::debug;

#[derive(Default)]
pub struct Publisher {
    // 键:事件类型;值:订阅ID到订阅者的映射
    subscribers: HashMap<EventType, HashMap<u64, Box<dyn Fn(&Event) + Send + Sync + 'static>>>,
    next_sub_id: u64,
}

impl Publisher {
    // 订阅事件,返回唯一订阅ID用于取消
    pub fn subscribe<F>(&mut self, event_type: EventType, listener: F) -> u64
    where
        F: Fn(&Event) + Send + Sync + 'static,
    {
        let sub_id = self.next_sub_id;
        self.next_sub_id += 1;
        
        self.subscribers
            .entry(event_type.clone())
            .or_default()
            .insert(sub_id, Box::new(listener));
        
        debug!("Subscribed to event {:?} (ID: {})", event_type, sub_id);
        sub_id
    }

    // 通过订阅ID取消订阅
    pub fn unsubscribe(&mut self, event_type: EventType, sub_id: u64) {
        if let Some(sub_map) = self.subscribers.get_mut(&event_type) {
            if sub_map.remove(&sub_id).is_some() {
                debug!("Unsubscribed from event {:?} (ID: {})", event_type, sub_id);
            }
        }
    }

    // 发送带载荷的事件通知
    pub(crate) fn notify(&self, event: &Event) {
        let event_type = event.event_type();
        if let Some(sub_map) = self.subscribers.get(&event_type) {
            for listener in sub_map.values() {
                listener(event);
            }
            debug!("Notified {} listeners about event {:?}", sub_map.len(), event_type);
        }
    }
}

3. 使用示例

// 定义订阅者
fn handle_element_a_created(event: &Event) {
    if let Event::ElementACreated(element) = event {
        println!("Element A created: {:?}", element);
    }
}

fn main() {
    let mut publisher = Publisher::default();
    
    // 订阅ElementACreated事件
    let sub_id = publisher.subscribe(EventType::ElementACreated, handle_element_a_created);
    
    // 发送带载荷的通知
    let new_element = ElementA {};
    publisher.notify(&Event::ElementACreated(new_element));
    
    // 取消订阅
    publisher.unsubscribe(EventType::ElementACreated, sub_id);
}

方案优势

  • 类型安全:订阅者通过匹配Event枚举获取对应载荷,编译器确保不会处理错误的载荷类型
  • 灵活订阅:基于EventType订阅,无需关心具体载荷实例,解决了HashMap键的限制问题
  • 扩展性强:新增事件只需扩展EventType和Event枚举,无需修改Publisher核心逻辑
  • 可靠取消:通过订阅ID取消订阅,支持函数和闭包类型的订阅者

内容的提问来源于stack exchange,提问作者user2037559

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 00:02:38