Crate he_di [] [src]

What is Dependency Injection (aka. Dependency Inversion)?

The idea behind inversion of control is that, rather than tie the classes in your application together and let classes “new up” their dependencies, you switch it around so dependencies are instead passed in during class construction. It's one of the 5 core principles of SOLID programming

If you want to read more on that:

Getting started

Structure your application

Start by writing a classical application with struct & types (in homage to AutoFac I ported their classical "getting started" example). Code excerpts are used below to illustrate this little guide, the complete example is available here.

trait IOutput {
    fn write(&self, content: String);
}
 
struct ConsoleOutput {
    prefix: String,
    other_param: usize,
}
 
impl IOutput for ConsoleOutput {
    fn write(&self, content: String) {
        println!("{} #{} {}", self.prefix, self.other_param, content);
    }
}
 
trait IDateWriter {
    fn write_date(&self);
}
 
struct TodayWriter {
    output: Box<IOutput>,
    today: String,
    year: String,
}
 
impl IDateWriter for TodayWriter {
    fn write_date(&self) {
       let mut content = "Today is ".to_string();
       content.push_str(self.today.as_str());
       content.push_str(" ");
       content.push_str(self.year.to_string().as_str());
       self.output.write(content);
    }
}

Mark structs as Component

A component is an expression or other bit of code that exposes one or more services and can take in other dependencies.

In our example, we have 2 components:

  • TodayWriter of type IDateWriter
  • ConsoleOutput of type IOutput

To be able to identify them as components he_di exposes a #[derive()] macro (though the he_di_derive crate). It is simply done using the following attributes:

#[derive(Component)] // <--- mark as a Component
#[interface(IOutput)] // <--- specify the type of this Component
struct ConsoleOutput {
    prefix: String,
    other_param: usize,
}

In the current version, you alos need to specify the type of your Component using the #[interface()] attribute.

Express dependencies

Some components can have dependencies to other components, which allows the DI logic to also inject these components with another Component.

In our example, ConsoleOuput is a Component with no dependency and TodayWriter a Component with a dependency to a IOutput Component.

To express this dependency, use the #[inject] attribute within your struct to flag the property and declare the property as a trait object.

In our example:

#[macro_use] extern crate he_di_derive;

#[derive(Component)] // <--- mark a struct as a Component that can be registered & resolved
#[interface(IDateWriter)] // <--- specify which interface it implements
struct TodayWriter {
    #[inject] // <--- flag 'output' as a property which can be injected
    output: Box<IOutput>, // <--- trait object using the interface `IOutput`
    today: String,
    year: usize,
}

Application startup

At application startup, you need to create a ContainerBuilder and register your components with it.

In our example, we register ConsoleOutput and TodayWriter with a ContainerBuilder doing something like this:

// Create your builder.
let mut builder = ContainerBuilder::new();
 
builder
    .register::<ConsoleOutput>()
    .as_type::<IOutput>();
 
builder
    .register::<TodayWriter>()
    .as_type::<IDateWriter>();
 
// Create a Container holding the DI magic
let mut container = builder.build().unwrap();

The Container reference is what you will use to resolve types & components later. It can then be stored as you see fit.

Application execution

During application execution, you’ll need to make use of the components you registered. You do this by resolving them from a Container with one of the 3 resolve() methods.

Passing parameters

In most cases you need to pass parameters to a Component. This can be done either when registring a Component into a ContainerBuilder or when resolving a Component from a Container.

You can register parameters either using their property name or their property type. In the later case, you need to ensure that it is unique.

When registering components

Passing parameters at registration time is done using the with_named_parameter() or with_typed_parameter() chained methods like that:

builder
    .register::<ConsoleOutput>()
    .as_type::<IOutput>()
    .with_named_parameter("prefix", "PREFIX >".to_string())
    .with_typed_parameter::<usize>(117 as usize);

When resolving components

Passing parameters at resolve time uses the same with_named_parameter() or with_typed_parameter() methods from your Container instance.

For our sample app, we created a write_date() method to resolve the writer from a Container and illustrate how to pass parameters with its name or type:

fn write_date(container: &mut Container) {
    let writer = container
        .with_typed_parameter::<IDateWriter, String>("June 20".to_string())
        .with_named_parameter::<IDateWriter, usize>("year", 2017 as usize)
        .resolve::<IDateWriter>()
        .unwrap();
    writer.write_date();
}

Now when you run your program...

  • The write_date() method asks he_di for an IDateWriter.
  • he_di sees that IDateWriter maps to TodayWriter so starts creating a TodayWriter.
  • he_di sees that the TodayWriter needs an IOutput in its constructor.
  • he_di sees that IOutput maps to ConsoleOutput so creates a new ConsoleOutput instance.
  • Since ConsoleOutput doesn't have any more dependency, it uses this instance to finish constructing the TodayWriter.
  • he_di returns the fully-constructed TodayWriter for write_date() to use.

Later, if we wanted our application to write a different date, we would just have to implement a different IDateWriter and then change the registration at app startup. We won’t have to change any other classes. Yay, inversion of control!

Roadmap

The current implementation of this crate is still WIP. A few identified usefull to know limitations (being further explorer) are:

  • #[derive(Component)] should be tested against complex cases & more tests are to be written (e.g, struct with lifetime, generics, ...)
  • we should support closures as a way to create parameters (at register or resolve time)

Modules

container

ContainerBuilder and Container structs used respectively to register and resolve Components.

parameter

AnyMap variants to pass parameters to a ContainerBuilder or Container.

Structs

Container

Struct containing all the components registered during the build phase, used to resolve Components.

ContainerBuilder

Build a Container registering components with or without parameters.

Enums

Error

This type represents all possible errors that can occur when registering or resolving components or when generating the code to do so.

Type Definitions

Result

Alias for a Result with the error type he_di::Error