0

I'm trying to implement a variant of the observer pattern. Currently I have it like this (after the example on Refactoring Guru here):

#[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); } } 

I would like to extend it in a way that would allow me to also send event-specific payloads along in the notify function.

Example:

#[derive(Debug, PartialEq, Eq, Hash, Clone)] pub enum Event { ElementACreated // <- Also sends ElementA in notify, ElementADeleted // <- Also sends ElementAId in notify, ElementBCreated // <- Also sends ElementB in notify, ElementBDeleted // <- Also sends ElementAId in notify, } pub type Subscriber = fn(event: Event, payload: <ElementA|ElementAId|ElementB|ElementBId>); // <-- This would need some kind of extension #[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, payload: <ElementA|ElementAId|ElementB|ElementBId>) { // <-- This as well let listeners = self.events.get(&event_type).unwrap(); for listener in listeners { listener(event_type.clone(), payload); } debug!("Notified {} listeners about event: {:?}", listeners.len(), event_type); } } 

I tried extenting the Event Enum like so:

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

But then the subscribers would only ever listen to one Id or one struct as the Hash in the Publisher::events HashMap would be to restricted.

Any ideas on how to glue Event Type and Payload together to create an ergonomic and slick solution would be greatly appreciated!

1 Answer 1

1

First of all, let's take a look at how we could make HashMap behave with enum variants with the same values even if the underlying data is different.

#[derive(Debug, Clone)] pub enum Event { ElementACreated(ElementA), ElementADeleted(ElementAId), ElementBCreated(ElementB>), ElementBDeleted(ElementBId), } 

It's fairly simple. To accomplish it need to implement Hash, PartialEq, and Eq with our own implementation instead of the derived one.

impl Hash for Event { fn hash<H: Hasher>(&self, state: &mut H) { match self { Event::ElementACreated(_) => { "ElementACreated".hash(state); } Event::ElementADeleted(_) => { "ElementADeleted".hash(state); } Event::ElementBCreated(_) => { "ElementBCreated".hash(state); } Event::ElementBDeleted(_) => { "ElementBDeleted".hash(state); } } } } // discriminant function returns a value uniquely identifying the enum variant impl PartialEq for Event { fn eq(&self, other: &Self) -> bool { std::mem::discriminant(self) == std::mem::discriminant(other) } } impl Eq for Event {} 

Then Event could be used as a key type for HashMap and as a data container. But here is a catch. Publisher accepts event in a subscribe function. pub fn subscribe(&mut self, event_type: Event, listener: Subscriber)

That leads to very inconvenient(bad) API design. The user must pass the event instance to the subscribe function.

let mut p = Publisher::default(); p.subscribe(Event::ElementACreated(...), |ev| { println!("Received event: {:?}", ev); }); 

To resolve this situation let's refactor the current design of the event to two enums and combine the structure with these two enums.

#[derive(Debug, Clone)] pub enum EventData { ElementA { id: ElementAId, name: String }, ElementAId(u64), ElementB { id: ElementBId, name: String }, ElementBId(u64), } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum EventType { ElementACreated, ElementADeleted, ElementBCreated, ElementBDeleted, } #[derive(Debug, Clone)] pub struct Event { event_type: EventType, data: EventData, } 

Then subscribe will look like this

pub fn subscribe(&mut self, event_type: EventType, listener: Subscriber) { self.events .entry(event_type.clone()) .or_default() .push(listener); println!("Subscribed to event: {:?}", event_type); } 

In notify, only data should be passed, and then the necessary event for that data can be determined.

pub(crate) fn notify(&self, data: EventData) { let event_type = match &data { EventData::ElementA { .. } => EventType::ElementACreated, EventData::ElementAId(_) => EventType::ElementADeleted, EventData::ElementB { .. } => EventType::ElementBCreated, EventData::ElementBId(_) => EventType::ElementBDeleted, }; let listeners = self.events.get(&event_type).unwrap(); for listener in listeners { listener(Event { event_type: event_type.clone(), data: data.clone(), }); } println!( "Notified {} listeners about event: {:?}", listeners.len(), event_type ); } 

P.S.

Also, Event could be obliterated, and Subscriber will pass EventType and EventData.

pub type Subscriber = fn(event_type: EventType, data: EventData); 
Sign up to request clarification or add additional context in comments.

2 Comments

This is great thank you! Is there a way to make it where one event type can only ever be sent with the same event data (type)?
@user2037559 For now notify already make sure that it will be sent with the correct EventType. It's a simple and elegant solution. If you want to be sure for 100% move Event to the separate module. play.rust-lang.org/…

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.