Dependency Injection in Rust

How to use traits to keep your code aloof, in plain english

Orion Kindel
Geek Culture

--

Photo by Erik Mclean on Unsplash

Who is this for?

  • Rust developers who have a some experience with the language and are curious about taking the next step in robustness
  • Non-Rust developers who are curious about Rust patterns

What will be covered?

  • Recap of traits and their use
  • Using traits to isolate (and substitute) dependencies

Why should you care?

Dependency injection allows us to write code that treats its dependencies like black boxes, knowing nothing about the nitty gritty details. This quality allows us to easily test and future-proof our applications! For example, if we want to interact with a database we would prefer:

over:

They may look similar but there’s an important distinction between accepting a loose trait vs. a hard struct: substitution.

In the former example, do_stuff will happily use any type that implements the Database trait, and in the latter, do_stuff will only accept the Database struct.

Traits — A recap

Traits are a name given to a group of functions that a data structure can implement.

For example, one of the traits from the standard library, Default, says “Anyone who implements me has a default value”

This says “In order for a type to implement Default, it needs to provide a function called default that takes no arguments and returns an instance of itself” (The Self type here is a special type that refers to whoever’s implementing Default)

For example, the default value for bool is false:

And the default value can be used like so:

What if you want to write a function that only cares about the input implementing a trait?

To answer this question, we’ll rely on the Debug trait that says “I can be formatted in a printable way for easy debugging”

Say we want to write a logger function that accepts anything Debug-able and prints it to standard output — there are a few ways of accomplishing this:

logger_impl and logger_generic are almost identical, and for most applications they mean the exact same thing.

logger_impl says “give me a t of some type that implements Debug

logger_generic says “for any type T that implements Debug, give me a t of type T

They both allow the compiler to replace impl Debug / T with the type you pass to it — for example if you call logger<bool>(true) it’s as if you wrote a fn: fn logger(t: bool) — there is no performance penalty.

logger_dyn is a little more special, this says “give me a t that points to some value on the heap that implements Debug.”

The compiler will know nothing about the specific type being used, and has to add a little bloat so the code can figure out where to look in memory for the trait functions.

This comes with a minor performance penalty, but would allow you to write a version of logger that accepts a collection of different types that all implement Debug;

Cool, now we’re speaking the same language and can move on to dependency injection!

Isolating dependencies with Traits

Let’s say we’re writing an application that should:

  • Get calendar events for the day from Outlook and Google Calendar
  • Create a daily summary of events from all sources
  • Send the summary to you via SMS, Slack, and Discord

Let’s start with getting calendar events by tying together Outlook and Google calendar under a common Calendar trait:

Now, the “Event Summary”:

Next, tie together the notification channels (SMS, Slack, Discord) under a common Notifier trait:

Now our main function needs to create the hard implementing types, and call out to a separate function isolated from the implementations:

Not only is our code clean, handling all calendars and notification channels the same way, but our business logic is now completely isolated from the implementations!

Testing run would be as simple as:

Feel free to ask any questions about using traits effectively, and follow me for a follow-up story on extending this pattern with the AppState pattern!

--

--

Orion Kindel
Geek Culture

Advocating for quality, user experience, and thoughtful design in code and in products!