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!
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);