rust 121 lines · 3 tabs

Type-Erased Command Handler Registry With Trait Objects in Rust

Shared by codesnips Jul 2026
3 tabs
use std::any::Any;

pub trait CommandHandler {
    fn name(&self) -> &str;

    fn handle(&self, input: &dyn Any) -> Box<dyn Any>;
}

pub trait Command: Any {
    fn as_any(&self) -> &dyn Any;
}

impl<T: Any> Command for T {
    fn as_any(&self) -> &dyn Any {
        self
    }
}

pub fn expect_input<'a, T: Any>(input: &'a dyn Any) -> &'a T {
    input
        .downcast_ref::<T>()
        .expect("command payload type did not match handler")
}
3 files · rust Explain with highlit

This snippet shows how a command dispatcher can be built around trait objects so that new command handlers plug into a central registry without the dispatcher knowing their concrete types. The problem it solves is decoupling: a subsystem wants to route named commands to handlers, but the set of handlers should be extensible and each may take a different payload and produce a different result.

In handler.rs, the core abstraction is the object-safe CommandHandler trait. Object safety is the constraint that makes Box<dyn CommandHandler> possible, so the trait avoids generic methods and instead operates on erased inputs: handle receives a &dyn Any and returns Box<dyn Any>. A name method supplies the registry key. The Command and helper traits keep the payload-to-Any conversion in one place. Using Any is the deliberate trade-off here — the caller regains concrete types through downcast, paying a small runtime check in exchange for a heterogeneous, runtime-composed handler set.

In registry.rs, the Registry wraps a HashMap<String, Box<dyn CommandHandler>>. The register method takes any H: CommandHandler + 'static, boxes it, and stores it under its own name(), so registration order and call sites stay decoupled. The dispatch method looks up the handler and calls handle, returning a DispatchError when the command name is unknown. Because the map owns boxed trait objects, the concrete handler types are fully erased after registration; only the trait vtable remains.

In main.rs, two concrete handlers, EchoHandler and SumHandler, implement CommandHandler with different payload and output types. They are registered into one Registry, then dispatched by name. The results come back as Box<dyn Any> and are recovered with downcast_ref, which returns None if the actual type does not match — a real pitfall to guard against when payloads and handlers drift apart. This pattern is worth reaching for when the handler set is open-ended (plugins, extensions, message routers); when the set is closed and known at compile time, an enum with a match is usually simpler and avoids the Any erasure entirely.


Related snips

Share this code

Here's the card — post it anywhere.

Type-Erased Command Handler Registry With Trait Objects in Rust — share card
Link copied