genericsrusttraitsfactory-patternabstraction

How to write an abstraction in Rust with multiple implementations


In my application I want to have an EventPersister trait that defines an abstraction of persisting events, then have various implementations that, for example, persist events in memory, in the file system, or in a database.

This is a cut-down version of my EventPersister trait:

pub trait Keyed {
    fn type_name(self: &Self) -> &'static str;
    fn key(self: &Self) -> String;
}

pub trait EventPersister {
    fn log<T>(self: &mut Self, event: &T) -> Result<(), ()>
    where T: Keyed + Serialize;
}

I have an implementation of this that persists events in memory:

pub struct LogEntry;

pub struct InMemoryEventPersister {
    entries: Vec<LogEntry>,
}

impl InMemoryEventPersister {
    pub fn new() -> Self {
        Self {
            entries: Vec::new(),
        }
    }
}

impl EventPersister for InMemoryEventPersister {
    fn log<T>(self: &mut Self, event: &T) -> Result<(), ()>
    where T: Keyed + Serialize
{
  --snip-
}

So far so good, but now I am stuck trying to write a function that will return a concrete implementation of the EventPersister trait depending on how the application is configured. For simplicity, the code below always returns the in-memory persister. Note that this code will not compile:

pub fn build_event_persister() -> &'static dyn EventPersister {
    &InMemoryEventPersister::new()
}

I tried many variations of this syntax, read all the docs and SO articles I can find, but nothing is pointing me in the right direction.

Creating an abstraction around some functionality with multiple implementation options is something I do a lot in other languages, where typically you define multiple classes that implement an interface. I'm finding it hard to believe that Rust doesn't support this very common pattern.

-- EDIT -- Nov 24, 2024

For further clarification of what I want to do, here is the full source code for the in-memory version. I combined all of my source files into one file here to make it more readable, adding mod{} sections to help the reader. This might have messed up some of the type references. In my application (with modules in separate source files) this code did compile and run without errors.

pub type Timestamp = u128;

pub struct LogEntry {
    pub timestamp: Timestamp,
    pub type_name: String,
    pub key: String,
    pub serialization: Option<Vec<u8>>,
}

pub struct EventQueryOptions {
    pub include_serialization: bool,
}

type EventQueryResult = Box<dyn Iterator<Item = LogEntry>>;

pub trait Keyed {
    fn type_name(self: &Self) -> &'static str;
    fn key(self: &Self) -> String;
}

pub trait EventPersister {
    fn log<T>(self: &mut Self, event: &T, timestamp: &Timestamp) -> Result<(), ()>
    where
        T: Keyed + Serialize;

    fn query_by_timestamp<'a>(
        self: &'a Self,
        start: &Timestamp,
        end: &Timestamp,
        options: &EventQueryOptions,
    ) -> EventQueryResult;

    fn query_by_key_prefix<'a>(
        self: &'a Self,
        key_prefix: &str,
        options: &EventQueryOptions,
    ) -> EventQueryResult;
}

mod in_memory {
    pub struct InMemoryEventPersister {
        entries: Vec<LogEntry>,
    }

    struct LogEntryIterator<'a, F> where F: Fn(&'a LogEntry) -> bool {
        entries: &'a Vec<LogEntry>,
        index: usize,
        filter: F,
    }

    impl InMemoryEventPersister {
        pub fn new() -> Self {
            Self {
                entries: Vec::new(),
            }
        }
    }

    impl<'a, F> LogEntryIterator<'a, F> where F: Fn(&'a LogEntry) -> bool {
        pub fn new(entries: &'a Vec<LogEntry>, filter: F) -> Self {
            Self {
                entries,
                filter,
                index: entries.len(),
            }
        }
    }

    impl<'a, F> Iterator for LogEntryIterator<'a, F>  where F: Fn(&'a LogEntry) -> bool {
        type Item = LogEntry;

        fn next(self: &mut Self) -> Option<Self::Item> {
            loop {
                self.index = self.index - 1;
                match self.entries.get(self.index) {
                    Some(entry) if (self.filter)(entry) => return Some(LogEntry {
                        key: entry.key.clone(),
                        timestamp: entry.timestamp.clone(),
                        type_name: entry.type_name.clone(),
                        serialization: entry.serialization.clone()
                    }),
                    Some(_) => continue,
                    None => return None,
                }
            }
        }
    }

    impl super::EventPersister for InMemoryEventPersister {
        fn log<T>(self: &mut Self, event: &T, timestamp: &data_types::Timestamp) -> Result<(), ()>
        where
            T: Keyed + Serialize
        {
            let mut buffer = Vec::new();
            let mut serializer = Serializer::new(&mut buffer);
            event.serialize(&mut serializer).unwrap();

            let log_entry = LogEntry {
                timestamp: timestamp.clone(),
                type_name: event.type_name().to_owned(),
                key: event.key(),
                serialization: Some(buffer),
            };

            self.entries.push(log_entry);
            Result::Ok(())
        }

        fn query_by_timestamp(
            self: &Self,
            start: &data_types::Timestamp,
            end: &data_types::Timestamp,
            _options: &EventQueryOptions,
        ) -> EventQueryResult {
            let iter = LogEntryIterator::new(
                &self.entries, 
                move|entry|entry.timestamp >= *start && entry.timestamp < *end);
            Box::new(iter);
        }

        fn query_by_key_prefix(
            self: &Self,
            key_prefix: &str,
            _options: &EventQueryOptions,
        ) -> EventQueryResult {
            let iter = LogEntryIterator::new(
                &self.entries,
                move|entry|entry.key.starts_with(key_prefix));
            Box::new(iter)
        }
    }
}

mod events {
    #[derive(Debug, Deserialize, Serialize)]
    pub struct Ack {
        pub message_ref: MessageRef,
        pub subscription_id: SubscriptionId,
        pub consumer_id: ConsumerId,
    }

    #[derive(Debug, Deserialize, Serialize)]
    pub struct Nack {
        pub message_ref: MessageRef,
        pub subscription_id: SubscriptionId,
        pub consumer_id: ConsumerId,
    }

    #[derive(Debug, Deserialize, Serialize)]
    pub struct Publish {
        pub message_ref: MessageRef,
    }

    impl super::Keyed for Ack {
        fn type_name(self: &Self) -> &'static str {
            "Ack"
        }
        fn key(self: &Self) -> String {
            self.message_ref.to_key()
        }
    }

    impl super::Keyed for Nack {
        fn type_name(self: &Self) -> &'static str {
            "Nack"
        }
        fn key(self: &Self) -> String {
            self.message_ref.to_key()
        }
    }

    impl super::Keyed for Publish {
        fn type_name(self: &Self) -> &'static str {
            "Publish"
        }
        fn key(self: &Self) -> String {
            self.message_ref.to_key()
        }
    }
}

Solution

  • Doing what you set out to do is quite difficult in current Rust, because trait objects are largely incompatible with generic methods. The best way to do it is to give up on trait object and make EventPersister an enum:

    enum EventPersister {
        InMemory(InMemoryEventPersister),
        File(FileEventPersister),
        // ...
    }
    
    impl EventPersister {
        pub fn log<T: Keyed + Serialize>(&mut self, event: &T) -> Result<(), ()> {
            match self {
                InMemory(p) => p.log(event),
                File(p) => p.log(event),
            }
        }
    }
    

    At this point you don't e need an EventPersister trait, though you could certainly have one if you find it useful. (In that case, look into the enum_dispatch crate which eliminates some of the boilerplate.) Functions like build_event_persister() would just return the EventPersister enum.

    If you really insist on dynamic dispatch, it's possible, but quite hard in general, and exceedingly hard in your case. The general technique to do it is described by David Tolnay as part of erased-serde, and is reasonably simple to apply to your EventPersister and Keyed crates without serialization. What makes your trait hard is that the generic method EventPersister::log() depends on another object-unsafe trait, Serialize. It should be possible to make use of erased-serde, but doing so in practice runs into several problems that are beyond the scope of this answer - I could only make it work by forking erased-serde and modifying the definition of the EventPersister trait, so that's not an approach worth pursuing.