In this lesson, you’ll learn how to build a centralized, type-safe registry to manage system-wide services without relying on loose, error-prone global variables.
The Problem: Global Chaos
In large modular systems, components often need to access shared services like a Logger, a Database, or a Configuration manager. If you pass these dependencies manually through every function signature, you end up with "prop drilling." Conversely, using raw global variables is unsafe and difficult to test. A Component Registry acts as a central hub where components can be registered and retrieved by their trait interfaces, ensuring type safety throughout the system.
Implementing the Registry
The secret to a robust registry is using Any for type-erasure combined with TypeId to ensure that when you request a service, you get exactly the implementation you expect.
use std::any::{Any, TypeId};
use std::collections::HashMap;
pub struct ComponentRegistry {
services: HashMap<TypeId, Box<dyn Any>>,
}
impl ComponentRegistry {
pub fn new() -> Self {
Self { services: HashMap::new() }
}
pub fn register<T: 'static>(&mut self, service: T) {
self.services.insert(TypeId::of::<T>(), Box::new(service));
}
pub fn get<T: 'static>(&self) -> Option<&T> {
self.services.get(&TypeId::of::<T>())
.and_then(|s| s.downcast_ref::<T>())
}
}
How It Works
- Type Erasure: We store services as
Box<dyn Any>. Since the registry doesn't know the concrete types ahead of time, this allows us to put anything that satisfies'staticinto the map. - Type Identification: By using
TypeId::of::<T>()as the key, we map the specific type to its implementation. This prevents collisions and allows us to enforce that the type requested is the type returned. - Downcasting: When calling
get, we usedowncast_ref. This safely verifies that the internal type matches the requested type before returning it. If the service isn't registered, it simply returnsNone, allowing your system to handle missing dependencies gracefully.
This approach keeps your system architecture modular. You can swap out a logging implementation during tests without changing the code that consumes it, as long as both implementations satisfy the same trait interface.
Key Takeaway: Use a HashMap of TypeId to Box<dyn Any> to create a type-safe central hub that decouples components from their concrete dependencies.