notes

Log | Files | Refs | README

dependency_injection.md (5513B)


      1 # Dependency Injection
      2 
      3 **Dependency injection** (DI) is a design pattern where a component receives
      4 its dependencies from an external source rather than creating them
      5 itself.[^1](https://dev.to/sgchris/how-traits-enable-dependency-injection-in-rust-5a50)
      6 This decouples components, making code easier to test, swap, and maintain.
      7 In Rust, DI is achieved idiomatically through **traits** and **generics**
      8 — without needing a framework — leveraging the compiler's type system to
      9 enforce correctness at compile
     10 time.[^2](https://jmmv.dev/2022/04/rust-traits-and-dependency-injection.html)
     11 
     12 ---
     13 
     14 ## Core Tool: Traits
     15 
     16 In Rust, **traits** act as the contract (interface) that dependencies must
     17 fulfill.[^1](https://dev.to/sgchris/how-traits-enable-dependency-injection-in-rust-5a50)
     18 Define the behaviour, not the implementation:
     19 
     20 ```rust
     21 pub trait Logger {
     22     fn log(&self, message: &str);
     23 }
     24 
     25 pub struct ConsoleLogger;
     26 
     27 impl Logger for ConsoleLogger {
     28     fn log(&self, message: &str) {
     29         println!("[LOG]: {}", message);
     30     }
     31 }
     32 ```
     33 
     34 ---
     35 
     36 ## Approach 1: Generics (Static Dispatch)
     37 
     38 The preferred Rust approach — the concrete type is resolved at **compile
     39 time**, producing zero-cost
     40 abstractions.[^1](https://dev.to/sgchris/how-traits-enable-dependency-injection-in-rust-5a50)
     41 
     42 ```rust
     43 pub struct Application<L: Logger> {
     44     logger: L,
     45 }
     46 
     47 impl<L: Logger> Application<L> {
     48     pub fn new(logger: L) -> Self {
     49         Self { logger }
     50     }
     51 
     52     pub fn run(&self) {
     53         self.logger.log("Application is running!");
     54     }
     55 }
     56 
     57 fn main() {
     58     let app = Application::new(ConsoleLogger);
     59     app.run(); // prints: [LOG]: Application is running!
     60 }
     61 ```
     62 
     63 `Application` only requires that `L` implements `Logger` — swap in any
     64 logger without touching `Application`
     65 itself.[^3](https://users.rust-lang.org/t/how-do-you-implement-dependency-injection-in-rust/213)
     66 
     67 ---
     68 
     69 ## Approach 2: Trait Objects (Dynamic Dispatch)
     70 
     71 When you need runtime flexibility or to store mixed implementations in a
     72 collection, use `Box<dyn
     73 Trait>`.[^3](https://users.rust-lang.org/t/how-do-you-implement-dependency-injection-in-rust/213)
     74 
     75 ```rust
     76 pub trait MessageSender {
     77     fn send(&self, msg: &str);
     78 }
     79 
     80 pub struct NotificationService {
     81     sender: Box<dyn MessageSender>,
     82 }
     83 
     84 impl NotificationService {
     85     pub fn new(sender: Box<dyn MessageSender>) -> Self {
     86         Self { sender }
     87     }
     88 
     89     pub fn notify(&self, msg: &str) {
     90         self.sender.send(msg);
     91     }
     92 }
     93 ```
     94 
     95 This introduces a small **runtime overhead** via vtable lookup, so prefer
     96 generics unless dynamic dispatch is genuinely
     97 needed.[^1](https://dev.to/sgchris/how-traits-enable-dependency-injection-in-rust-5a50)
     98 
     99 ---
    100 
    101 ## Approach 3: Enums (Closed Set)
    102 
    103 If you have a fixed, known set of implementations, an enum avoids both
    104 generics complexity and boxing
    105 overhead.[^3](https://users.rust-lang.org/t/how-do-you-implement-dependency-injection-in-rust/213)
    106 
    107 ```rust
    108 pub enum LoggerKind {
    109     Console,
    110     File(String),
    111 }
    112 
    113 impl Logger for LoggerKind {
    114     fn log(&self, message: &str) {
    115         match self {
    116             LoggerKind::Console => println!("[Console]: {}", message),
    117             LoggerKind::File(path) => println!("[File({})] {}", path, message),
    118         }
    119     }
    120 }
    121 ```
    122 
    123 ---
    124 
    125 ## Why DI Shines in Testing
    126 
    127 The real payoff is **mockability** — swap the real implementation with a
    128 mock during
    129 tests.[^1](https://dev.to/sgchris/how-traits-enable-dependency-injection-in-rust-5a50)
    130 
    131 ```rust
    132 pub struct MockLogger {
    133     pub messages: std::cell::RefCell<Vec<String>>,
    134 }
    135 
    136 impl Logger for MockLogger {
    137     fn log(&self, message: &str) {
    138         self.messages.borrow_mut().push(message.to_string());
    139     }
    140 }
    141 
    142 #[test]
    143 fn test_app_logs_on_run() {
    144     let mock = MockLogger { messages: Default::default() };
    145     let app = Application::new(&mock);
    146     app.run();
    147     assert!(mock.messages.borrow().contains(&"Application is running!".to_string()));
    148 }
    149 ```
    150 
    151 No real I/O, no external systems — pure, fast unit
    152 tests.[^1](https://dev.to/sgchris/how-traits-enable-dependency-injection-in-rust-5a50)
    153 
    154 ---
    155 
    156 ## Choosing the Right Approach
    157 
    158 |                                        | Generics              | `Box<dyn Trait>`        | Enum                   |
    159 | :------------------------------------- | :-------------------- | :---------------------- | :--------------------- |
    160 | **Dispatch**                           | Compile time (static) | Runtime (dynamic)       | Compile time (static)  |
    161 | **Overhead**                           | Zero                  | Vtable lookup           | Zero                   |
    162 | **Implementations**                    | Any (open set)        | Any (open set)          | Fixed (closed set)     |
    163 | **Heterogeneous collections**          | ❌                    | ✅                      | ✅                     |
    164 | **Best for**                           | Most cases            | Plugin-like flexibility | Known, finite variants |
    165 
    166 For complex dependency graphs, consider a DI container crate such as
    167 [rustyinject](https://github.com/AlexSherbinin/rustyinject).[^4](https://github.com/AlexSherbinin/rustyinject)
    168 
    169 ---
    170 
    171 ## Key Pitfall: Visibility Creep
    172 
    173 Any type referenced in a **public** trait's function signature must also be
    174 public.[^2](https://jmmv.dev/2022/04/rust-traits-and-dependency-injection.html)
    175 If your trait is public but references an internal struct, you'll be forced
    176 to expose that struct too — keep internal traits `pub(crate)` where
    177 possible.[^2](https://jmmv.dev/2022/04/rust-traits-and-dependency-injection.html)