1

I'm new to Rust and coming from C++ background. Trying to design an interface, I'm undecided between 2 approaches:

struct Handler {
    name: String,
    invoke: fn()
}

struct MyHandler1;
impl MyHandler1 {
    fn new() -> Handler {
        name: String::from("myhandler1"),
        invoke: || { println!("hello") }
    }
}
(...)
const MY_HANDLERS: &[Handler] = &[MyHandler1::new(), MyHandler2::new()];

and

trait Handler {
    fn name(&self) -> String;
    fn invoke(&self);
}

struct MyHandler1;
impl Handler for MyHandler1 {
    fn name(&self) -> String { "myhandler1".to_string() }
    fn invoke(&self) {
        println!("hello");
    }
}
(...)
// can't have const fn in traits so box'em up inside main...
let handlers: Vec<Box<dyn Handler>> = vec![Box::new(MyHandler1], Box::new(MyHandler2)];

I'm leaning towards the former example, but I can't shake the feeling that it's coming from a deeply ingrained C++ way of thinking, having all the functions and members nicely stacked up inside classes and simulating type erasure by the new function that generates a "base class" instead of trying to get used to thinking in Rust.

It feels like Rust encourages using functions instead of objects (traits instead of inheritance). Is there an established good practice in Rust community to this end? Are both approaches considered OK and just a matter of preference? Or are there alternatives that you use to achieve this kind of design? (Please ignore trivial syntax errors)

corsel
  • 315
  • 2
  • 12
  • You'll likely run into problems calling `new` and stuff in the first method, there isn't a lot you are able to do in const methods. Is there a reason you have traits with `MyHandlerX` instead of just a single handler struct which you can change the fields of? – Jeremy Meadows Aug 12 '22 at 15:27
  • Is there a particular reason you're worried about `const`ness? – isaactfa Aug 12 '22 at 15:28
  • No specific reason for const, thought might be safer to have a static container, but I understand it may cause hassle since I'll need to have everything const inside handlers. And regarding why I try traits; I can practically go either way, just not sure the former approach has implications I'm not aware of or discouraged for some reason. – corsel Aug 12 '22 at 15:33
  • Will each handler only have a name and a closure? You could use a lazy static map, and then that would allow you to call the closure by name. – Jeremy Meadows Aug 12 '22 at 15:40
  • @JeremyMeadows not sure whether it'll grow but please elaborate that does sound like a handy solution. – corsel Aug 12 '22 at 15:44

2 Answers2

2

It's certainly perfectly idiomatic to use trait objects, there doesn't seem to be a good reason to do so in this instance.

So I would go with the first approach. However, it's not a Rust pattern to use "constructor structs" like your MyHandler1. Instead, your Handler struct should provide a constructor:

struct Handler {
    name: String,
    // Generally, it's better to take closures than function pointers as they
    // are more versatile. This does necessitate to box it.
    invoke: Box<dyn Fn()>,
}

impl Handler {
    // Constructors are by convention called `new` in Rust.
    fn new(name: String, invoke_with: String) -> Self {
        Self {
            name,
            invoke: Box::new(move || {
                println!("{}", invoke_with);
            }),
        }
    }

    // You can just add a method for any special type of Handler
    // you want to commonly make.
    fn new_handler_of_type_1() -> Self {
        Self::new("myhandler1".to_string(), "hello".to_string())
    }

    fn new_handler_of_type_2() -> Self {
        Self::new("myhandler2".to_string(), "goodbye".to_string())
    }
}

And then you can call it like this:

fn main() {
    let my_handlers = &[
        Handler::new("mycustomhandler".to_string(), "custom msg".to_string()),
        Handler::new_handler_of_type_1(),
        Handler::new_handler_of_type_2(),
    ];
}
isaactfa
  • 5,461
  • 1
  • 10
  • 24
  • My first concern was whether it would be possible to add a new handler from another crate or module, but I see that's possible through an `extend` crate. Also thanks for the link, chapter 17 seems to have good detailed explanations for my case. – corsel Aug 15 '22 at 07:45
  • 1
    @corsel If you own the `Handler` trait, you don't need `extend`. You can implement a trait that you own for any type from any crate. – isaactfa Aug 15 '22 at 08:00
  • Can you explain what exactly "owning the trait" means? Same expression is in the description of extend crate but I don't get it. – corsel Aug 15 '22 at 09:09
  • 1
    "Owning a trait" just means that you defined it in your crate. You _don't_ own the `Iterator` trait for example because it's defined in `std`. In Rust, generally speaking, you can implement a trait you own for a type you own, implement a trait you **don't** own for a type you **do** own, and implement a trait you **do** own for a type you **don't** own. You just can't implement a trait you don't own for a type you don't own. This is called the [orphan rules](https://github.com/Ixrec/rust-orphan-rules#what-are-the-orphan-rules) if you want to dive in deeper. – isaactfa Aug 15 '22 at 09:14
2

Even if it doesn't need to grow, mapping the name (the string) to the closure might be a nice ease-of-use. Depending on how much data your Handler struct needs which wasn't in your example, you might could combine the approaches:

#![feature(once_cell)]

use std::collections::BTreeMap;
use std::lazy::SyncLazy;

type Closure = dyn Fn() -> () + Sync + Send;

static HANDLERS: SyncLazy<BTreeMap<&str, Box<Closure>>> = SyncLazy::new(|| {
    let mut map = BTreeMap::<&str, Box<Closure>>::new();

    map.insert("handler1", Box::new(|| println!("hello")));
    map.insert("handler2", Box::new(|| println!("goodbye")));

    map
});

fn main() {
    HANDLERS.get("handler1").unwrap()();
}

I'm using the SyncLazy from the nightly compiler, but once_cell and lazy_static are other options, or you could just have it in main.

Jeremy Meadows
  • 2,314
  • 1
  • 6
  • 22