Fluent Rust - Constructor Injection

Migo·2023년 9월 8일
1

Constructor Injection?

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 disassemble class into struct for data and impl for attached behaviours. So the use of class is for your understanding.

Hello Writer Example - static dispatch

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)
    }
}

Complete code example

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();
}

Hello Writer Example - dynamic dispatch

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.

profile
Dude with existential crisis

0개의 댓글

관련 채용 정보