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)