Injecting Objects
Lockjaw can create objects for you, but you need to let lockjaw know how to create the object and what is needed to create them. These recipes for object creation is called bindings, which forms nodes in a dependency graph, and add edges to other bindings it depends on.
The most simple binding is constructor injection binding.
Constructor injection
A binding can be created for a struct by using a constructor method, which is a static method
associated to the struct that returns a new instance. The field constructor ( Foo {a : 0, ...}
) is
not directly used by Lockjaw since methods are more expressive when a none injected field needs a
default value or when transformations are needed on the input.
A struct can be made injectable by marking a struct impl
block with
the #[injectable]
attribute, and
then mark the constructor method
as #[inject]
.
struct Foo {}
#[injectable]
impl Foo {
#[inject]
pub fn new() -> Foo {
Foo {}
}
}
Now Lockjaw understands when trying to create Foo
, it should call Foo::new()
.
Note that since it is an associated method, constructor injection only works on a type you own ( you can actually change its implementation). For foreign types like imported crates a different method will be discussed in the providing objects chapter.
Constructor dependencies
To create an object, it may many need objects of other types. This is called dependencies. In this
example, Bar
depends on having an instance of Foo
to be created.
In constructor injections, dependencies are listed with its parameters. Lockjaw will try to use available bindings to create all arguments and pass them to the constructor. If a parameter does not have a binding, Lockjaw will fail compilation with missing bindings.
Note that since we are not asking Lockjaw to actually create the object yet, binding validation won't be performed. Lockjaw is assuming there are some bindings else where it does not know about yet.
struct Bar {
foo: Foo,
i: i32,
}
#[injectable]
impl Bar {
#[inject]
pub fn new(foo: Foo) -> Bar {
Bar { foo, i: 42 }
}
}
If the struct has other fields that can be initialized without injection, like i
, it can be
directly assigned. If the object needs a runtime value (for example, if i
needs to be assigned by
the caller with a user input), then factories will be needed, which will be discussed
later.
Manual injection
For a moment let's forget about Lockjaw, and try to do dependency injection manually. With the binding information we have we can write a factory that can create the objects we just defined:
struct Factory {}
impl Factory {
pub fn create_foo(&self) -> Foo {
Foo::new()
}
pub fn create_bar(&self) -> Bar {
Bar::new(self.create_foo())
}
}
#[test]
fn test() {
let factory = Factory {};
let _foo = factory.create_foo();
let _bar = factory.create_bar();
}
Note that there is one method for each binding, only taking &self
and returning the binding type.
Inside the method it calls the constructor method we just marked, and calls other binding methods to
generate the argument.
The factory is an object instead of just methods, since it might need to carry states in the future (For example, returning a reference to a shared object owned by the factory.)
Writing the factory by hand gets complicated and boring fast. In the next chapter we will ask Lockjaw to generate it.
Source of this chapter