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 disassembleclass
intostruct
for data andimpl
for attached behaviours. So the use ofclass
is 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
.