Advanced Trait-Based Abstractions
Advanced trait-based abstractions combine static dispatch, dynamic trait objects (dyn Trait), associated types, and adapter structs to build zero-cost composable pipelines (such as middleware stacks, query combinators, and type-erased plugin registries) that bridge generic compile-time performance with runtime flexibility.
Pattern 1: Zero-Cost Composable Middleware / Layer Pipelines
Instead of storing a Vec<Box<dyn Middleware>> that performs a heap allocation and vtable jump at every layer, a generic adapter struct Layered<Inner, Middleware> wraps an inner service by value. The compiler monomorphizes and inlines the entire pipeline into a single flat function call:
Static Composition (`Timeout<Logging<CoreService>>`):
+-------------------------------------------------------+
| TimeoutLayer |
| timeout_ms: u64 |
| +-----------------------------------------------+ |
| | LoggingLayer | |
| | prefix: &'static str | |
| | +---------------------------------------+ | |
| | | CoreHandler (0 heap pointers!) | | |
| | +---------------------------------------+ | |
| +-----------------------------------------------+ |
+-------------------------------------------------------+
pub trait Handler {
type Request;
type Response;
fn handle(&self, req: Self::Request) -> Self::Response;
}
// Extension trait for fluent `.with_logging(...)` composition:
pub trait HandlerExt: Handler + Sized {
fn with_logging(self, tag: &'static str) -> Logged<Self> {
Logged { inner: self, tag }
}
}
impl<H: Handler> HandlerExt for H {}
pub struct Logged<H> {
inner: H,
tag: &'static str,
}
impl<H> Handler for Logged<H>
where
H: Handler,
H::Request: std::fmt::Debug,
H::Response: std::fmt::Debug,
{
type Request = H::Request;
type Response = H::Response;
fn handle(&self, req: Self::Request) -> Self::Response {
println!("[{}] -> request: {:?}", self.tag, req);
let resp = self.inner.handle(req);
println!("[{}] <- response: {:?}", self.tag, resp);
resp
}
}
struct SquareService;
impl Handler for SquareService {
type Request = i32;
type Response = i32;
fn handle(&self, req: i32) -> i32 {
req * req
}
}
fn main() {
let stack = SquareService.with_logging("outer").with_logging("inner");
println!("size of composed stack: {} bytes", size_of_val(&stack));
let out = stack.handle(9);
println!("final output: {}", out);
}
size of composed stack: 32 bytes
[inner] -> request: 9
[outer] -> request: 9
[outer] <- response: 81
[inner] <- response: 81
final output: 81
Pattern 2: Bridging Non-Dyn-Compatible Traits into dyn Trait (Type-Erased Bridge)
When a trait is not dyn-compatible—for example, Clone (because fn clone(&self) -> Self returns Self by value) or a trait with associated types—you can bridge it into a dyn-compatible trait by adding a helper trait DynClone whose method returns Box<dyn MyTrait> and providing a blanket implementation for all T: MyTrait + Clone:
trait Job: DynCloneJob {
fn run(&self) -> String;
}
trait DynCloneJob {
fn clone_box(&self) -> Box<dyn Job>;
}
// Blanket implementation automatically implements `DynCloneJob` for every
// concrete `T` that implements `Job + Clone + 'static`!
impl<T> DynCloneJob for T
where
T: Job + Clone + 'static,
{
fn clone_box(&self) -> Box<dyn Job> {
Box::new(self.clone())
}
}
// Now we can implement standard `Clone` for `Box<dyn Job>`!
impl Clone for Box<dyn Job> {
fn clone(&self) -> Self {
self.clone_box()
}
}
#[derive(Clone)]
struct ReportJob {
title: String,
}
impl Job for ReportJob {
fn run(&self) -> String {
format!("running report {:?}", self.title)
}
}
fn main() {
let original: Box<dyn Job> = Box::new(ReportJob {
title: String::from("Q4 Revenue"),
});
// `Box<dyn Job>` can now be cloned directly via `.clone()`!
let cloned: Box<dyn Job> = original.clone();
println!("original: {}", original.run());
println!("cloned: {}", cloned.run());
}
original: running report "Q4 Revenue"
cloned: running report "Q4 Revenue"