Generally speaking, would you prefer a runtime pass you a channel endpoint that you read system events from, or would you prefer passing a trait object that will have its methods called when system events occur?
The message passing variant has the upside that it's a little more compact, and if the Receiver<> has both blocking and async receiving methods then it can be passed to and used from an async context. Also, the trait variant would need two traits; one async and one non-async, if one would want async callbacks.
However, I get the feeling that traits are more idiomatic in the Rust ecosystem.
(Performance isn't really important here, these events will happen very rarely -- it's just an ergonomics and aesthetics question).
Variant 1 - "Message passing via channel"
enum Msg {
Pause,
Resume,
ReloadConf,
Stop
}
trait Runtime {
fn run(&mut self, evt: Receiver<Msg>)
}
struct MyApp { }
impl Runtime for MyApp {
fn run(&mut self, evt: Receiver<Msg>) {
// application must recv() from evt to handle system events
}
}
fn main() {
let app = MyApp { };
// will end up calling Runtime::run()
svc::run(app);
}
Variant 2 - "Callback trait"
trait EvtHandler {
fn pause(&mut self);
fn resume(&mut self);
fn reload_conf(&mut self);
fn stop(&mut self);
}
trait Runtime {
fn run(&mut self);
}
struct MyApp { }
impl Runtime for MyApp {
fn run(&mut self) {
}
}
struct MyHandler { }
impl EvtHandler for MyHandler {
fn pause(&mut self) { }
fn resume(&mut self) { }
fn reload_conf(&mut self) { }
fn stop(&mut self) { }
}
fn main() {
let app = MyApp { };
let evth = MyHandler { }
// will end up calling Runtime::run() and kick off a background thread to call EvtHandler methods
svc::run(app, evth);
}
It's definitely more idiomatic the trait way, the users don't have to deal with channels and all the error handling and matching on events that entails. eventually your users will just end up creating the trait-based system themselves if they aren't provided with one
If you choose the trait (or any form of callback, like a boxed function), it's important to think about what happens if calling one of the trait methods does something surprising, like panic, or call any of your other functions, because you're putting the user-supplied code in the middle of your code. Providing a channel doesn't have this disadvantage, but does mean you have to implement/choose exactly what form of channel receiver you provide.
Also, the author of a callback can simply make use of a channel sender of their own when they wish, whereas turning a channel receiver into executing some code requires setting up a task or thread.
I would also recommend considering a third option combining aspects of both: don't define a trait with four individual methods, but use the enum Msg. The advantages of this are flexibility for the user of your library:
They can do something before/after every message easily, or delegate to another event handler.
You can let them provide a simple callback function instead of a custom trait implementation, which can be more concise and convenient. (But it's not any more or less powerful.)