Implementing strategy to invoke functions at runtime

Hi all, I'm trying to do the following:

  • I have a few objects that implement a trait: Handler
  • each Handler can Handle a Command
    Now I want to link handler to command in such a way that I can invoke something like
associations.get(command.getName()).handle(command)

a Command is defined as follows

pub trait Command {
    fn name() -> & 'static str;
}

an handler is defined as follows

pub trait Handler<T: Command> {
    type Result;

    fn handle(&mut self, msg: T) -> impl Future<Output = Self::Result> + Send;
}

Now, I want to implement Command1 that is executed for CommandHandler1 and Command2 that is executed for CommandHandler2. So far so good.

My problem is that I want a facility, I was thinking an hashmap, where the keys are the names of the command (str) and the value is the actual instance of a CommandHandler, such that given a Command that I've built in a function, I could use this hashmap to invoke the corresponding CommandHandler. But I get a lot of troubles with the typing because if I try to implement such an hashmap with the type HashMap<&str, Handler> the compiler complains because Handlers cannot be made into objects.
All I want to do here is really making sure that the value implements this trait so that I can just invoke the handle function.

Is there a good way to achieve this in rust? I'm happy to change approach if there is a better way to achieve the same result

I think there are two things to solve here:

  1. Storing different concrete implementations using a Box<dyn Handler>:

    use std::collections::HashMap;
    
    let mut associations = HashMap::<&'static str, Box<dyn Handler<_>>>::new();
    associations.put("one", Box::new(handler1));
    associations.put("two", Box::new(handler2)); // can be different concrete types
    
  2. Implementing Handler<T> for different concrete Command types

    In the above snippet, we don't have enough information to tell Rust what goes in the _ in Box<dyn Handler<_>>.

    We could make it Box<dyn Handler<Box<dyn Command>>>, which means the Handler implementations would be something like:

    impl Handler<Box<dyn Command>> for HandlerOne {
        // ..
    }
    

    which may or may not be good enough for your use case -- do Handlers need to know the concrete type of the Command, or is the box enough?

(more questions than answers! :grimacing:)

I've tried to implement the same in the playground so that people can see what's the problem, as suggested in the thread Rust Playground

Written before your recent playground, based on the OP.


If you want Handler<_> to be object safe, you need to type erase instead of using -> impl Trait.

pub trait ErasedHandler<T: Command> {
    type Result;
    fn handle_erased(&mut self, msg: T)
        -> Pin<Box< dyn '_ + Send + Future<Output = Self::Result> >>
    ;
}

That lets you get a dyn ErasedHandler<Cmd, Result = Res> where Cmd is a specific Command implementer, and Res is also a specific type.

If you want to be able to handle different Command implementors with the same dyn ErasedHandler<..> type, you'll have to be able to type erase Cmd by making Command object safe, too. (Or alternatively use an enum if you know all the implementors.)

pub trait ErasedCommand {
    fn name_erased(&self) -> &'static str;
}

// Replaced `<T: Command>` with `&dyn ErasedCommand`
pub trait ErasedHandler {
    type Result;
    fn handle_erased(&mut self, msg: &dyn ErasedCommand)
        -> Pin<Box< dyn '_ + Send + Future<Output = Self::Result> >>
    ;
}

That lets you get a dyn ErasedHandler<Result = Res>.

If you want to handle different Result types, you'll need an enum or to type erase that, too.

#[non_exhaustive]
pub enum HandlerResult {
    String(String),
    // ...
    Opaque(Box<dyn Any + Send>),
}

// Replaced `Result` with `HandlerResult`
pub trait ErasedHandler {
    fn handle_erased(&mut self, msg: &dyn ErasedCommand)
        -> Pin<Box< dyn '_ + Send + Future<Output = HandlerResult> >>
    ;
}

Now you can get a dyn ErasedHandler.


Sometimes one can provide blanket implementations from Trait to ErasedTrait, but there's no obvious way to do so in the case of ErasedHandler once <T: Command> has been erased. As presented, a Handler can choose which Commands they support, but an ErasedHandler has to support any (erased) ErasedCommand.

If Handlers had to handle all Commands (and some other adjustments), you may be able to provide a blanket implementation.

2 Likes

in general (in CQRS for what I know at least) for each command handler there is only one command associated to it. So in that sense they can know each other.
However I was trying a way to generalize this using traits (but miserably failing doing so)

One thing to notice is that each concrete implementation of Handlers can have their own extra collaborators they depend on (like in the rust playground link I've shared)

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.