Intro to Dependency Injection in Rust
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