Dependency inversion in Rust

Given a struct MyStruct living within module my_struct, and a trait Feature living in the module my_feature, it is required that MyStruct holds different implementations of the trait Feature without depending on these implementations.

Our goal will be to tell MyStruct that it may accept any struct that implements Feature via new() method as parameter.

⁠In other languages —mostly with structural typing— the footprint of this new() method would look like this:

fn new(feature: Feature) -> MyStruct;

This won't work in Rust, as it can't infere the size of Feature at compile time.

But this may be achieved via two different strategies:

  • Using generics
  • Using trait objects

Dependency inversion with generics

It is possible to let the compiler pass the type of Feature as a generic.

A first step would be to define Feature trait within feature module:

⁠mod feature {
⁠ pub trait Feature {
⁠ fn action(&self) -> String;
⁠ }
⁠}

Feature module

Within my_struct module would live MyStruct. It would receive any generic of type Feature, which can be seen in impl <T: Feature>....

MyStruct will then receive T as generic; now it will be possible to type feature parameter as T.

⁠mod my_struct {
⁠ use super::feature::Feature;

⁠ pub struct MyStruct<T> {
⁠ feature: T,
⁠ }

⁠ impl<T: Feature> MyStruct<T> {
⁠ pub fn new(feature: T) -> MyStruct<T> {
⁠ MyStruct { feature }
⁠ }

⁠ pub fn action(&self) -> String {
⁠ self.feature.action()
⁠ }
⁠ }
⁠}

MyStruct module

The different implementations of Feature would live in different modules:

mod feature_implementation01 {
⁠ use super::feature::Feature;

⁠ pub struct FeatureImplementation01;

⁠ impl Feature for FeatureImplementation01 {
⁠ fn action(&self) -> String {
⁠ String::from("Feature 01 - with generics")
⁠ }
⁠ }
⁠}

First implementation of Feature


⁠mod feature_implementation02 {
⁠ use super::feature::Feature;

⁠ pub struct FeatureImplementation02;

⁠ impl Feature for FeatureImplementation02 {
⁠ fn action(&self) -> String {
⁠ String::from("Feature 02 - with generics")
⁠ }
⁠ }
⁠}

First implementation of Feature

Finally the client code would consume this logic:


⁠pub fn main() {
⁠ use feature_implementation01::FeatureImplementation01;
⁠ use feature_implementation02::FeatureImplementation02;
⁠ use my_struct::MyStruct;

⁠ let feature_implementation_01_instance = FeatureImplementation01;
⁠ // If required, it is possible to pass the type of
⁠ // the implementation into the generic with turbofish syntax
⁠ let my_struct =
⁠ MyStruct::<FeatureImplementation01>::new(
⁠ feature_implementation_01_instance
⁠ );
⁠ let result = my_struct.action();
⁠ println!("{:#?}", result);

⁠ ⁠// If required, it is possible to pass the type of
⁠ // the implementation into the generic with turbofish syntax
⁠ let feature_implementation_02_instance = FeatureImplementation02;
⁠ let my_struct =
⁠ MyStruct::<FeatureImplementation01>::new(
⁠ feature_implementation_02_instance
⁠ );
⁠ let result = my_struct.action();
⁠ println!("{:#?}", result);
⁠}

Run in Rust playground 

It is important to note that when MyStruct::new is called, it is receiving the type of the struct implementing Feature with the turbofish syntax:

let my_struct = 
⁠ MyStruct::<FeatureImplementation01>::new(
⁠ feature_implementation_01_instance
⁠ );

Instantiating MyStruct injecting an instance implementing Feature

⁠Rust compiler may be able to infere it. This is the case with this code: if we remove ::<FeatureImplementation01>, the compiler will be able to find out which type is passed; but it is not always the case, and sometimes the developer may be required to pass it manually.

⁠With Smart pointers

Rust provides several smart pointers for dinamically sized structures. The obvious choice here is Box.
The required changes to achieve this will hapen only on my_struct module and within the client code, as seen below.
It is required to remove the generics on MyStruct; the issue then will be that feature is typed as the trait Feature: but the size of structs implementing this trait may vary.
⁠This can be solved by wrapping Feature within Box<dyn Feature>⁠, telling the compiler to send this struct into the heap, which is dinamically sized. To be explicit about traits being dinamically dispatched Rust forces us to use the dyn keyword.

mod my_struct {
⁠ use super::feature::Feature;

⁠ pub struct MyStruct {
⁠ feature: Box<dyn Feature>,
⁠ }

⁠ impl MyStruct {
⁠ pub fn new(feature: Box<dyn Feature>) -> MyStruct {
⁠ MyStruct { feature }
⁠ }

⁠ pub fn action(&self) -> String {
⁠ self.feature.action()
⁠ }
⁠ }
⁠}

Typing feature as Box<dyn Feature>

The client code will change as well, as it requires to wrap the feature instance within a Box instance:

fn main() {
⁠ use feature_implementation01::FeatureImplementation01;
⁠ use feature_implementation02::FeatureImplementation02;
⁠ use my_struct::MyStruct;

⁠ let feature_implementation_01_instance = FeatureImplementation01;
⁠ let my_struct = MyStruct::new(Box::new(feature_implementation_01_instance));
⁠ let result = my_struct.action();
⁠ println!("{:#?}", result);

⁠ let feature_implementation_02_instance = FeatureImplementation02;
⁠ let my_struct = MyStruct::new(Box::new(feature_implementation_02_instance));
⁠ let result = my_struct.action();
⁠ println!("{:#?}", result);
⁠}

Run in Rust Playground 

Now, lets say MyStruct makes use of MyService, which at the same time requires the feature instance hold within its struct.

mod my_service {
⁠ use super::feature::Feature;

⁠ pub struct MyService {
⁠ feature: Box<dyn Feature>,
⁠ }

⁠ impl MyService {
⁠ pub fn new(feature: Box<dyn Feature>) -> MyService {
⁠ MyService { feature }
⁠ }
⁠ }
⁠}

MyService, which requires a Feature instance

mod my_struct {
⁠ use super::feature::Feature;
⁠ use super::my_service::MyService;

⁠ pub struct MyStruct {
⁠ feature: Box<dyn Feature>,
⁠ }

⁠ impl MyStruct {
⁠ pub fn new(feature: Box<dyn Feature>) -> MyStruct {
⁠ MyStruct { feature }
⁠ }

⁠ pub fn action(self) -> String {
⁠ let my_service = MyService::new(self.feature);
⁠ println!("{}", my_service.action());

⁠ self.feature.action().to_owned()
⁠ }
⁠ }
⁠}

MyStruct making use —and depending on— of MyService

Now the compiler returns an error, as MyService is borrowing self.feature:

⁠error[E0382]: borrow of moved value: `self.feature`
⁠ --> src/with_box.rs:25:7
⁠ |
⁠21 | let my_service = MyService::new(self.feature);
⁠ | ------------ value moved here
⁠...
⁠25 | self.feature.action().to_owned()
⁠ | ^^^^^^^^^^^^^^^^^^^^^ value borrowed here after move

Error when consuming self.feature

When passing self.feature to MyService it is consumed, so it wont be available after this line. A solution would be to clone() it; but the compiler will return a new error:

error[E0599]: the method `clone` exists for struct 
⁠ `Box<(dyn with_box::feature::Feature + 'static)>`,
⁠ but its trait bounds were not satisfied
⁠ --> src/with_box.rs:21:52
⁠ |
⁠2 | pub trait Feature {
⁠ | -----------------
⁠ | |
⁠ | doesn't satisfy `dyn with_box::feature::Feature: Clone`
⁠ | doesn't satisfy `dyn with_box::feature::Feature: Sized`
⁠...
⁠21 | let my_service = MyService::new(self.feature.clone());
⁠ | ^^^^^ method
⁠ cannot be called on
⁠ `Box<(dyn with_box::feature::Feature + 'static)>`
⁠ due to unsatisfied trait bounds

The trait Box doesnt implement Clone. It would be possible to implement Clone for Box<dyn Feature>, but there is a simpler solution: use the Rc pointer instead of Box.

⁠mod my_struct {
⁠ use super::feature::Feature;
⁠ use super::my_service::MyService;
⁠ use std::rc::Rc;

⁠ pub struct MyStruct {
⁠ feature: Rc<dyn Feature>,
⁠ }

⁠ impl MyStruct {
⁠ pub fn new(feature: Rc<dyn Feature>) -> MyStruct {
⁠ MyStruct { feature }
⁠ }

⁠ pub fn action(self) -> String {
⁠ let my_service = MyService::new(self.feature.clone());
⁠ println!("{}", my_service.action());

⁠ self.feature.action().to_owned()
⁠ }
⁠ }
⁠}

Run in Rust playground 

Rc implements Clone, so we can call it over self.feature.

A last case should be taken into account: the code may be asynchronous; but Rc is not thread safe.

The following examples will make use of Tokio as async runtime as well as async_trait to be able to create traits with asynchronous methods.

For example, if we require MyStruct to implement a trait MyStructTrait, the compiler will warn that both MyStructTrait and Feature should implement Send + Sync :

mod my_struct {
⁠ use super::feature::Feature;
⁠ use async_trait::async_trait;
⁠ use std::rc::Rc;

⁠ pub struct MyStruct {
⁠ feature: Rc<dyn Feature>,
⁠ }

⁠ #[async_trait]
⁠ pub trait MyStructTrait: Send {
⁠ fn new(feature: Rc<dyn Feature>) -> Self;
⁠ async fn action(&self) -> Result<String, String>;
⁠ }

⁠ #[async_trait]
⁠ impl MyStructTrait for MyStruct {
⁠ fn new(feature: Rc<dyn Feature>) -> Self {
⁠ MyStruct { feature }
⁠ }

⁠ async fn action(&self) -> Result<String, String> {
⁠ self.feature.action().await
⁠ }
⁠ }
⁠}

Async code using Rc

error: future cannot be sent between threads safely
⁠ --> src/with_arc.rs:31:54
⁠ |
⁠31 | async fn action(&self) -> Result<String, String> {
⁠ | ______________________________________________________^
⁠32 | | self.feature.action().await
⁠33 | | }
⁠ | |_____^ future created by async block is not `Send`

Error on async code with Rc

It is possible to tell the compiler to extend both Feature and MyStructTrait with Send + Sync; there will be another obstacule though: x cannot be sent between threads safely.

error[E0277]: `Rc<(dyn with_arc::feature::Feature + 'static)>` 
⁠ cannot be sent between threads safely
⁠ --> src/with_arc.rs:26:8
⁠ |
⁠26 | impl MyStructTrait for MyStruct {
⁠ | ^^^^^^^^^^^^^ `Rc<(dyn with_arc::feature::Feature +
⁠ 'static)>
⁠ ` cannot be sent between threads safely

Error when implementing Send + Sync

The pointer Rc is useful on sync code, but is not thread safe: luckily for this Rust provides Arc, which, as per docs: «... provides shared ownership of a value of type T, allocated in the heap. […] Unlike Rc<T>, Arc<T> uses atomic operations for its reference counting. This means that it is thread-safe.»

Using Arc has more memory consumption than Rc, but for this case is negligible. And as implements Clone it will be possible to perform self.feature.clone() in order to pass it to Service.

⁠mod my_struct {
⁠ use super::feature::Feature;
⁠ use async_trait::async_trait;
⁠ use std::sync::Arc;

⁠ pub struct MyStruct {
⁠ feature: Arc<dyn Feature>,
⁠ }

⁠ #[async_trait]
⁠ pub trait MyStructTrait: Send + Sync {
⁠ fn new(feature: Arc<dyn Feature>) -> Self;
⁠ async fn action(&self) -> Result<String, String>;
⁠ }

⁠ #[async_trait]
⁠ impl MyStructTrait for MyStruct {
⁠ fn new(feature: Arc<dyn Feature>) -> Self {
⁠ MyStruct { feature }
⁠ }

⁠ async fn action(&self) -> Result<String, String> {
⁠ self.feature.action().await
⁠ }
⁠ }
⁠}

Using Arc, run in Rust Playground 

Conclusion

The convenience of generics over trait objects will depend on the context. Generics will have better performance, as its size is known at compile time and will use static dispatch; trait objects, on the other hand, will be send into the heap and will use dynamic dispatch, wich will have some memory overhead. But the flexibility it provides may worth it for many cases.

In short: each pointer —Box, Rc or Arc— will allow different actions:

  • Box: synchronous code where the trait object instance will be consumed once.
  • Rc: synchronous code where the trait object instance will have to be cloned in order to feed different factories.
  • Arc: asynchronous code that requires thread safety.

With these four options —generics, Box, Rc and Arc— it is possible to perform dependency inversion and apply the patterns that may be required in our codebase.
⁠The full source code for the four examples can be found at  git.antoniodiaz.me/antoniodcorrea/rust-dependency-inversion .