Collections of different structs keyed by trait

Hi

As part of my Rust learning process, I'm trying to convert a pattern I've used several times. This is a repository of "services" that are registered by a key (usually an interface name). This repository is then able to be queried to retrieve a "service" which can then be used. I've used this in the past as a simple DI method to allow the implementations to be replaced with mocks during tests.

I'm having a bit of trouble implementing a simple version of this in Rust.

What I would like to see is this sort of code:

// The service contract
trait DoSomethingService {
    fn do_something(&self);
}

// The service implementation
struct DoSomethingServiceImpl {}

impl DoSomethingService for DoSomethingServiceImpl {
    fn do_something(&self) {
        println!("Doing something for MyService");
    }
}

// BaseService is a marker trait
impl BaseService for DoSomethingServiceImpl {}


fn main() {
    let mut repo = ServiceRepo::new();

    let service1 = DoSomethingServiceImpl {};

    // Register our implementation with the repo as the trait DoSomethingService
    repo.register_service::<DoSomethingService>(Box::new(service1));

    // Get the service from the repo and have it cast to the trait NOT the concrete type
    if let Some(x) = repo.get_service::<DoSomethingService>() {
        x.do_something();
    }
}

My current implementation looks like this

#[derive(Default)]
struct ServiceRepo {
    services: HashMap<String, Box<dyn BaseService>>,
}

impl ServiceRepo {
    fn new() -> Self {
        Self {
            ..Default::default()
        }
    }

    fn register_service<T: BaseService>(&mut self, service: Box<dyn BaseService>) {
        self.services
            .insert(std::any::type_name::<T>().to_string(), service);
    }

    fn get_service<T: BaseService>(&self) -> Option<&Box<T>> {
        let service = self.services.get(&std::any::type_name::<T>().to_string());

        // TODO: Want to return an Option<&T> here
        // Not sure what to do!
    }
}

I "think" I'm storing the service implementation fine but retrieving it pre-cast as the required trait is evading me.

So a couple of questions:

  1. Is this a reasonable pattern in Rust? If not why not?
  2. How would I implement this? What am I currently missing?

It's not currently possible to directly downcast from a supertrait object to a subtrait object, if that is what you are looking for. Downcastin is only possible from dyn Any to concrete, sized types (ie., not trait objects or slices).

All of this seems to be way more dynamic than it needsto be, anyway. Usually, you'd first attempt using generics for polymorphism.

2 Likes

You can use type-map or similar to store Box<dyn Trait1>, Box<dyn Trait2> and so on.

Your sample seems to illustrate some confusion between traits and types. Traits aren't types. Implementors of a trait can be coerced to dyn Trait in the correct circumstances, and dyn Trait is a type. dyn Trait is dynamically sized and can perform dynamic dispatch, but is still a concrete type known at compile time. Some amount of dynamic typing can be emulated using Any.

There is no sub/supertype relationship between traits and their implementors, or between traits and supertraits or implementors of those.

Aggressively erasing types and attempting downcasts all over the place can be a symptom of trying to make Rust look like OOP or be dynamic.

2 Likes

Looking up objects by type from a registry is a common pattern in Java, which has runtime reflection (i.e. the ability to ask an object what type it is). Rust has no reflection (except when using Any). In addition, Java's interfaces are types, but Rust's traits are not.

Maybe one could achieve something similar using Any, but it feels like overkill.

How about doing something like this:

trait Service {
    const KEY: usize;
}

struct BlueService {}

impl Service for BlueService {
    const KEY: usize = 1;
}

fn main() {
    println!("{}", BlueService::KEY);
}

What you want is possible:

#[derive(Default)]
struct ServiceRepo {
    services: HashMap<TypeId, Box<dyn Any>>
}

impl ServiceRepo {
    fn new() -> ServiceRepo {
        ServiceRepo {
            ..Default::default()
        }
    }

    fn register_service<T: ?Sized + 'static>(&mut self, b: Box<T>) {
        self.services.insert(TypeId::of::<T>(), Box::new(b));
    }

    fn get_service<T: ?Sized + 'static>(&self) -> Option<&T> {
        let service = self.services.get(&TypeId::of::<T>())?;
        Some(service.downcast_ref::<Box<T>>().unwrap())
    }
}

Use like so:

fn main() {
    let mut repo = ServiceRepo::new();
    let service1 = DoSomethingServiceImpl;
    repo.register_service::<dyn DoSomethingService>(Box::new(service1));

    if let Some(x) = repo.get_service::<dyn DoSomethingService>() {
        x.do_something();
    }
}

Some notes:

  • Run-time type information requires the Any trait. Converting to something like dyn BaseService loses information about the original type, so there's no way to convert back.
  • The documentation warns against using the string returned from type_name as a unique key, so I swapped it for TypeId

That's basically type-map without the convenience.

You don't need to double-box sized things.

One can manually add ways to convert back to the base type to one's trait (though an immediate question is "why'd you type erase it then"; sometimes there's reasons but sometimes you're swimming against the tide).

Good point. So this, then:

#[derive(Default)]
struct ServiceRepo {
    services: HashMap<TypeId, Box<dyn Any>>
}

impl ServiceRepo {
    fn new() -> ServiceRepo {
        ServiceRepo {
            ..Default::default()
        }
    }

    fn register_service<T: 'static>(&mut self, b: T) {
        self.services.insert(TypeId::of::<T>(), Box::new(b));
    }

    fn get_service<T: 'static>(&self) -> Option<&T> {
        let service = self.services.get(&TypeId::of::<T>())?;
        Some(service.downcast_ref::<T>().unwrap())
    }
}

Although using it is less pretty:

fn main() {
    let mut repo = ServiceRepo::new();
    let service1 = DoSomethingServiceImpl;
    repo.register_service::<Box<dyn DoSomethingService>>(Box::new(service1));

    if let Some(x) = repo.get_service::<Box<dyn DoSomethingService>>() {
        x.do_something();
    }
}
1 Like