Intro to Dependency Injection in Rust

Money Grows on Trees
2 min readJul 3, 2020

Photo by Wojciech Then on Unsplash

Let’s start with a struct Client that contains a verifier struct which helps us perform verification on the responses fetched by the client

struct Verifier {}impl Verifier {
fn verify() {
println!("Verification Successful!");
}
}

pub struct Client {
verifier: Verifier
}

What’s wrong with the above implementation?

let’s say tomorrow, we want to change the implementation of how we verify responses in the Client; it will require us to change the Verifier struct

But we also need to think about testing? How do you unit test something like this? We cannot provide our mock Verifier implementation when testing

Let’s change the implementation a little bit

struct Verifier {}impl Verifier {
fn verify() {
println!("Verification Successfull!");
}
}
pub struct Client {
verifier: Verifier
}
impl Client {
fn constructor(verifier: Verifier) -> Self {
return Client {
verifier: verifier
}
}
}

This does not change anything - we are injecting the Verifier into Client but it is still the same struct with the same verify() implementation (so the behaviour is still the same)

We need something that lets us customize the verify() function

Traits to the rescue

A trait lets us define shared behaviour by defining methods that any type can implement (even the ones you did not define!)

We can define a trait that includes the Verification behaviour

pub trait Verification {
fn verify();
}

Now we can implement this trait for any type and we can pass it to our Client constructor which allows us to create different Client instances with varying Verification behaviour

pub struct Client {
verifier: Box<dyn Verification>
}
impl Client {
fn constructor(verifier: Box<dyn Verification>) -> Self {
return Client {
verifier: verifier
}
}
}

What is this Box<dyn ..> thing?

Box helps us allocate memory on the heap and then puts the object in the box in the allocated memory

We need it because any type can implement the Verification trait and the compiler cannot know the size of the passed type at compile time

Now, the above implementation lets us pass any type that implements the Verification trait which helps with testing and also allows separation of concerns

// Usage of Client Structpub struct FailingVerifier {}impl Verification for FailingVerifier {
fn verify() {
println!("Failing Verification")
}
}
fn main() {
let client = Client::constructor(Box::new(FailingVerifier{}));
}

That marks the end of the article, please feel free to point out in the comments if I am wrong about anything

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Responses (1)

Write a response