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