Providing Objects

In this chapter we will discuss another way to create bindings.

Limitations of constructor injection bindings

Constructor injections are designed to be the only way to create an instance of the type it binds. It is even considered bad practice to call the constructor manually. Hence, constructor injections has some limitations:

  • Constructor injections can only be done by owned types (can only be defined by the mod that defines the type itself.).
    • If you don't own the type you should not say something is the only way to create it.
  • Can only create concrete types
    • Sometimes you may want to bind traits and swap the implementation, maybe at runtime.

Modules

Obviously Lockjaw is not going to ask the world to use it or the user to rewrite everything they use with it, so it gives other ways to bind types. Since these bindings are no longer the "one true way to create things", and different bindings for the same type may be needed within the same program, the user needs to be able to select which bindings to use in each dependency graph.

In Lockjaw, these elective bindings are defined in modules, and the component can choose what modules to install, which imports its bindings. Note that in Lockjaw documentation modules always refer dependency injection modules, and mod will be used to refer to Rust modules.

To declare a module with Lockjaw the #[module] attribute should be used to mark the impl block of a struct.

struct MyModule {}

#[module]
impl My Module {
  ...
}

The impl block will contain the binding definitions.

For now the modules should be static (without fields). Modules with fields will be discussed in builder modules

#[provides] bindings

The #[provides] binding annotates a method that returns the type.

    #[provides]
    pub fn provide_i32() -> i32 {
        42
    }

Like #[inject], the #[provides] method can also request other bindings from the dependency graph, and produce the target value with it.

    #[provides]
    pub fn provides_string(i: i32) -> String {
        format!("{}", i)
    }

Installing modules

#[module] on its own is just a collection of bindings and does not do anything. It must be installed in a #[component] to joint the dependency graph. This is done by listing the module type in the modules metadata of the component.

#[component(modules: [MyModule])]
trait MyComponent {
    fn i32(&self) -> i32;

    fn string(&self) -> String;
}

A lot of Lockjaw attribute macros also takes metadata arguments, which is comma separated key : value pairs in a parenthesis. The values are usually string literal, integers, types, arrays of values (foo : [value 1, value 2]), or more metadata (foo : { key : value }). In this case modules takes an array of types (of #[modules]).

Providing trait is a bit more complicated and will be discussed later.

Source of this chapter