Firstly, I think I think I understand ownership and borrowing concepts to a fair extent. So, my purpose in this post is not to ask "How do I fix this?", I'd rather like to ask "How do I design this in a way that I can avoid this trap to a fair extent?".
I try to write a yet another dependency injection library with Rust these days. Let me show the code, then elaborate:
use std::any::Any;
use crate::module::Module; // this is just an empty struct, a WIP, will be implemented later
/// A function that takes a reference of the module and
/// produces an instance of a dependency.
type Producer = Box<dyn Fn(&Module) -> Box<dyn Any>>;
/// A struct defining the dependency.
struct Dependency<'a> {
/// The reference to the module that this dependency is under.
module: &'a Module,
/// The instance of the dependency.
instance: Option<Box<dyn Any>>,
/// Whether the dependency is singleton or not.
/// If the dependency is singleton, it will return
/// the same reference even if you produce it again.
is_singleton: bool,
/// The producer to produce the instance of dependency.
producer: Producer,
}
impl<'a> Dependency<'a> {
fn new(module: &'a Module, producer: Producer, is_singleton: bool, is_lazy: bool) -> Self {
debug!("Constructing dependency...");
let instance = if is_lazy {
None
} else {
debug!("Initializing the dependency...");
Some(producer(module))
};
Self {
module,
instance,
is_singleton,
producer,
}
}
/// This method produces and/or returns the instance of the dependency.
fn get(&mut self) -> &Box<dyn Any> {
debug!("Getting the instance of dependency...");
if self.is_singleton {
match self.instance {
Some(_) => {
debug!("Instance has already been created and is set as singleton.");
debug!("The reference to the singleton instance will return.");
self.instance.as_ref().expect("msg") // TODO msg
}
None => {
debug!("Instance has not been created yet.");
debug!("Initializing the singleton instance of dependency...");
self.instance = Some((self.producer)(self.module));
self.instance.as_ref().expect("msg") // TODO msg
}
}
} else {
debug!("Initializing the instance of dependency...");
self.instance = Some((self.producer)(self.module));
self.instance.as_ref().expect("msg") // TODO msg
}
}
}
Let me explain. The dependencies are stored in a struct conveniently named Dependency
. This struct has a couple of properties, the rationale of which can be seen below:
Dependency Struct |
Rationale |
---|---|
producer |
A Box<dyn Fn(&Module) -> Box<Any> to produce a dependency. |
is_singleton |
Whether it is a singleton or not. |
instance |
An Option<Box<Any>> that represents the instance.- If is_eager is passed to Dependency::new , this will be produced instantly on initialization.- If is_singleton is passed, the instance will not be produced a second time, resulting in the same data in the same address. |
module |
A &Module to pass down to producer invocation when Dependency::get is called. |
The problematic method in question is Dependency::get
. It is a &mut self
method. Naturally, it will not compile if it's called the second time in the same scope, hence the test below...
// compare if objs are in the same location in memory
#[rstest]
fn test_singleton(test_module: Module, #[values(true, false)] is_singleton: bool) {
let mut dep = Dependency::new(&test_module, Box::new(|_| Box::new(0)), is_singleton, true);
let obj1 = dep.get();
let obj2 = dep.get(); // (a)
let should_be_same = is_singleton;
assert_eq!(obj1 as *const _ == obj2 as *const _, should_be_same);
}
...will fail miserably on line (a) since we cannot borrow 'dep' as mutable more than once at a time
.
On the other hand, Dependency::get
has to be &mut self
because (i) I set Dependency.is_instance
in there and (ii) the instance has to be owned by some struct in order to be returned as reference in Dependency::get
.
Possible Question: Why do you even return a reference from Dependency::get
anyway?
T
and not &T
, the instance will be owned by the caller. This is not a desirable approach for singletons.So, as I've said in the beginning, "How do I fix this?" is not the problem here. Is this kind of architecture wrong? Should I take another path? Maybe, a single Dependency
struct for both singleton dependencies and factory dependencies is not a good solution at all. Maybe, I need to define different struct for each of them such as SingletonDependency
and FactoryDependency
. I can't comprehend such an approach would solve these kind of issues though, what do you think?
Thanks in advance.