Scoped Bindings
By default everytime a dependency needs to be satisfied, lockjaw creates a new instance, and move it to the dependency (field or method parameter). This is not always desired since an object may be used to carry some common state, and we want every type that depends on it to get a reference to a single instance instead (singletons).
In the last chapter, we had to use a mutable static state to store the messages TestLogger
logs,
since we need to read the same messages later but a different instance of TestLogger
will be
created.
To do this, the scope
metadata
can be specified on a #[injecatable]
or #[provides]
, passing a component's path. This means there are only one instance of the type for objects created
by the same instance of component (they are not global singletons, you can still have multiple
instances if you have multiple components).
#[derive(Default)]
pub struct TestLogger {
messages: RefCell<Vec<String>>,
}
#[injectable(scope: TestComponent)]
impl TestLogger {
#[inject]
pub fn new() -> TestLogger {
TestLogger::default()
}
pub fn get_messages(&self) -> Vec<String> {
self.messages.borrow().clone()
}
}
impl Logger for TestLogger {
fn log(&self, msg: &str) {
self.messages.borrow_mut().push(msg.to_owned())
}
}
Other types can depend on a scoped type as a reference (&T
) or Cl<T>
fn test_logger(&self) -> Cl<TestLogger>;
}
#[test]
fn test() {
let component: Box<dyn TestComponent> = <dyn TestComponent>::build();
component.greeter().greet();
assert_eq!(
component.test_logger().get_messages(),
vec!["helloworld!".to_owned()]
);
}
}
epilogue!();
Although #[binds]
has to explicitly ask for &TestLogger
#[binds]
pub fn bind_test_logger(_impl: &TestLogger) -> Cl<dyn Logger> {}
Note that Greeter
hasn't changed at
all. Cl<T>
allows a type to decouple itself
from whether the type depended on is scoped or not. It may be an owned instance or a shared
instance, but the type does not care as it will not try to move it.
Lifetime
Scoped objects are owned by the component and has the same lifetime as it.
Handling mutability
In most uses a scoped type probably should be mutable to make it useful. However we cannot request
it as &mut T
since certainly multiple objects will try to request it. Scoped types must
implement interior mutability itself
and use an immutable interface. In the example TestLogger
use
a RefCell
so the messages can be mutated
even when the TestLogger
itself is immutable.
Sometimes it might be easier to wrap the whole class in a memory container like a RefCell
or RwLock
.
The container
metadata
can be used on a #[injectable]
to
bind the type as &CONTAINER<T>
instead of &T