In Object-Oriented Programming world, we often use Constructor Injection pattern, which is where you define a list of required dependencies by specifying them as paramters when constructing a class. In this post, I'll be showing you a tiny little and simple example of how you may implement constrctor injection in Rust.
Well, Rust doesn't have
class, as it disassembleclassintostructfor data andimplfor attached behaviours. So the use ofclassis for your understanding.
Here, we have a struct called Sender with send method.
struct Sender;
impl Sender {
fn send(&self) {...}
}
But as it turns out, to send things out, you have to write it first. So, the sender should need argument to construct itself.
struct Sender{
writer : // How?
};
impl Sender {
fn send(&self) {...}
}
The trouble is, there are lots of writer such as console writer, buffer writer, and so on. So, following the LISKOV Substitution principle, you want to make Sender struct depend on interface/abstraction rather than concrete implementation.
Here is how you could do:
struct Sender<W:Writer>{
writer: W
};
trait Writer {
fn write(&self, word: &str);
}
impl Sender {
fn send(&self) {
self.writer.write("Send this!");
}
}
For the actual implementation, let's see the ConsoleWriter example.
struct ConsoleWriter;
impl Writer for ConsoleWriter {
fn write(&self, word: &str) {
println!("{}", word)
}
}
struct Sender<W: Writer> {
writer: W,
}
impl<W: Writer> Sender<W> {
fn send(&self) {
self.writer.write("Hello DI!")
}
}
trait Writer {
fn write(&self, word: &str);
}
struct ConsoleWriter;
impl Writer for ConsoleWriter {
fn write(&self, word: &str) {
println!("{}", word)
}
}
fn main() {
let writer = ConsoleWriter;
let sender = Sender { writer };
sender.send();
}
You can do the pretty much the same thing by dynamically dispatching object into heap, taking smart pointer such as Box as an argument which obviates the need for knowing the size of actual Writer implementation.
struct Sender {
writer: Box<dyn Writer>,
}
impl Sender {
fn send(&self) {
self.writer.write("Hello DI!")
}
}
trait Writer {
fn write(&self, word: &str);
}
struct ConsoleWriter;
impl Writer for ConsoleWriter {
fn write(&self, word: &str) {
println!("{}", word)
}
}
fn main() {
let writer = ConsoleWriter;
let sender = Sender {
writer: Box::new(writer),
};
sender.send();
}
With this implementation, the code got a little more cleaner. Assuming arguments passed in is not just one writer but multiple other interfaces, you may find this approach more ergonomic.
The cons of this is, however, it relies on late binding so there is a bit of performance hit.
Wait, then does that mean static dispatch doesn't get performance hit? And the short answer is yes it doesn't. For the curious, read up on documentation about monomorphization.