Ergonomic compile time dependency injection with shaku 0.2.0

crate, docs, repo, rocket integration

Shaku is a dependency injection library which I initially released earlier this month. Unlike most other Rust DI libraries, shaku has compile time guarantees and allows you to create both long living and temporary services.

This 0.2.0 release brings compile time DI guarantees, meaning all service dependencies and parameters will be checked during compilation (previously checked during app startup). The program will fail to compile if you don't supply a dependency/parameter or if you try to resolve a service which is not available. In addition, circular dependencies can be checked for at compile time.

Most of the work is taken care of by (optional) macros, so there is minimal boilerplate. See the getting started guide on the docs for a walkthrough!

use shaku::{module, Component, Container, ContainerBuilder, Interface};
use std::sync::Arc;

trait Output: Interface {
    fn write(&self, content: String);
}

trait DateWriter: Interface {
    fn write_date(&self);
}

#[derive(Component)]
#[shaku(interface = Output)]
struct ConsoleOutput;

impl Output for ConsoleOutput {
    fn write(&self, content: String) {
        println!("{}", content);
    }
}

#[derive(Component)]
#[shaku(interface = DateWriter)]
struct TodayWriter {
    #[shaku(inject)]
    output: Arc<dyn Output>,
    today: String,
    year: usize,
}

impl DateWriter for TodayWriter {
    fn write_date(&self) {
        self.output.write(format!("Today is {}, {}", self.today, self.year));
    }
}

module! {
    MyModule {
        components = [ConsoleOutput, TodayWriter],
        providers = []
    }
}

fn main() {
    let container: Container<MyModule> = ContainerBuilder::new()
        .with_component_parameters::<TodayWriter>(TodayWriterParameters {
            today: "Feb 23".to_string(),
            year: 2020
        })
        .build();

    let writer: &dyn DateWriter = container.resolve_ref();
    writer.write_date(); // Prints "Today is Feb 23, 2020"
}

I'm really interested in what you think of the API, usability, documentation, etc, so please open issues!

Reddit thread

1 Like

Thanks for sharing. What I wouldn't mind right at the top of the getting started is basically describe the problem this solves. Like a design issue most programmers can relate to and then go usually this is solved like this, but look with shaku the solution is so much nicer.

That way it becomes kind of clear what it excels at, and when you should use it and when not.

I have to admit that it sounds kind of interesting yet I don't immediately see when I could have used this to solve a problem that I struggled with...

2 Likes

Did you check the dependency injection link at the top of the post? It goes to a Wikipedia article which describes the general pattern. Dependency injection provides separation of concerns and easier testing, while a framework like shaku builds upon that to reduce the boilerplate required and automate some pieces. You may not have found a use for a dependency injection library in personal projects, as they are usually more useful in larger projects. For example, projects using the Java web framework Spring usually utilize the built-in dependency injection framework.

Ok, I had missed that link. I do know the concept, and would usually do that like:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=2b63910237d16be5528c30d1966d3e42

which does seem to have less boilerplate, but maybe it requires something more complex for shaku to shine. I just don't see it I suppose.

DI frameworks give you a pull-based approach to dependency injection. In your example, you're manually creating the components, while a DI framework would let you take a more declarative approach.
An example where shaku would perform better is if you have a lot of services using a database service, and you want to run some tests while using an in-memory database. Shaku would let you just call one function to override that service, and the new in-memory database service would be used. Without shaku, you would have to create the services again starting with a different base database service.
Shaku also integrates well with web frameworks. In the example of Rocket, you can simply add the services you want to use to the route parameters:

#[get("/")]
fn hello(hello_world: Inject<HelloModule, dyn HelloWorld>) -> String {
    hello_world.greet()
}

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.