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
相关产品推荐
相关产品推荐

