I'm interested in the options for providing ownership or references to dependencies when doing dependency injection in Rust. I don't mean like a DI framework or anything like that, just the general pattern.
For example let's say we have a trait for a storage layer that stores binary blobs.
pub trait BlobStore {
fn read_blob(&self, id: &str) -> Result<Vec<u8>, ()>;
}
Now we want to create a decorator blob store that wraps another, adding a caching layer. It needs to accept both a blob store and an in-memory cache.
pub trait Cache {
fn get(&self, key: &str) -> Option<Vec<u8>>;
fn set(&self, key: &str, value: Vec<u8>);
}
The question is: what should the decorator look like in terms of how it accepts the wrapped blob store and the cache? Here are the options I see, and their downsides:
Perhaps the most straightforward choice is to accept ownership of them:
pub struct CachingBlobStore<BS, C> { wrapped: BS, cache: C, } impl<BS: BlobStore, C: Cache> BlobStore for CachingBlobStore<BS, C> { fn read_blob(&self, id: &str) -> Result<Vec<u8>, ()> { todo!(); } }This is easy to implement, but it forces the creator to hand over ownership when they might not want to. For example, what if they also want to use the same cache object somewhere else in the program? In order to work around this they'd need to do something like the following:
// Workaround for wanting to reuse a cache elsewhere. impl<C> Cache for Arc<C> where C: Cache, { fn get(&self, key: &str) -> Option<Vec<u8>> { self.get(key) } fn set(&self, key: &str, value: Vec<u8>) { self.set(key, value) } } // Create a caching blobstore without needing to hand // over ownership of the cache. fn create_with_shared_cache<BS: BlobStore, C: Cache>(wrapped: BS, cache: Arc<C>) -> impl BlobStore { CachingBlobStore { wrapped, cache } }In fact I don't think they could do this from an outside package. The workaround would need to be even more cumbersome by defining a struct that wraps
Arc<C>.Instead we could bake in the fact that we're using
Arc, and only require shared ownership via reference counting.pub struct CachingBlobStore<BS, C> { wrapped: Arc<BS>, cache: Arc<C>, }But this requires the user to do a separate allocation for every dependency, and adds indirection that may or may not be an important runtime cost. It also forces the cost of reference counting on the user, even if they don't otherwise need it.
We could just require reference to each dependency, and make it the user's problem to figure out the lifetimes.
pub struct CachingBlobStore<'a, BS, C> { wrapped: &'a BS, cache: &'a C, }But this forces the user to keep the dependencies alive and in scope even if they just want to hand off ownership. It also adds an indirection cost that may or may not be important at runtime.
Is there any way to have our cake and eat it too? If there some option I haven't thought of here that allows the user to have flexibility in choosing what ownership model they want to use, without the downside of them having to define the relevant trait for a smart pointer wrapper struct if they want to hand over a smart pointer?
Arcor a reference, so ownership is generally superior. If references and/or arc's can't implement the trait due to orphan rules then creating aSharedCacheandCacheRef<'a>yourself is possible and reasonable.