4 ms·
Dependency Injection Demystified
- mrkeen 3y ago> Dependency injection ... is a handy concept that’s often explained terribly Dependency injection is putting the dependencies in the constructor, hence: > a 25-dollar term for a 5-cent concept. > Dependency injection, a.k.a. DI DI is dependency inversion. OK - so what are we "inverting" then? We are inverting the typical direction of dependencies FROM high-level->low-level TO low-level->high-level. By "typical", I mean the one you'd reach for first. For example: A HeadChef has a dependency on a SousChef. The HeadChef can getIngredients() from the SousChef. The SousChef is even injected into the HeadChef, per dependency injection. Then it comes time to write your unit tests. You decide (rightly) that's it's too much work to design an entire SousChef just for the purposes of testing the HeadChef. One of two things happens here: 1. You modify the code a little to use an interface/test-double. SousChef and TestSousChef both implement ISousChef. 2. You use a mocking framework to pretend you have a real SousChef, and just override its behaviours one-by-one as you encounter NPEs in your test case. This is the state in which most software I typically see ends up. HeadChef is coupled to SousChef. Using an ISousChef instead is coupled-but-with-extra-steps. And the mocking framework just adds brittleness. Fast-forward a little. SousChef is a little slow, so now we add hitWithWoodenSpoon() and berate() so that the HeadChef can get ingredients faster. And we update the interfaces and the mocks. And the coupling gets unmistakable tighter. Eventually the SousChef calls in sick, and the HeadChef completely shuts down. Despite the SousChef being injected, the HeadChefs methods are still littered with direct calls to the (I)SousChef. So.... inversion? Rather than the HeadChef depending downward on a (I)SousChef, the HeadChef should point upward at something way more abstract, i.e. A supplier of Ingredients. (Sometimes the 'upward'/'downward' metaphor doesn't exactly pan out, so DI is sometimes phrased as "A high-level class shouldn't depend on a low-level class; both should depend on (high-level) abstractions".) Three things happen when you do this: 1) Testability goes way up. A Supplier<Ingredient> is so simple to mock that it's probably going to be a lambda. No Mockito. No ISousChef. 2) New implementations suddenly become available. The HeadChef can get ingredients from the fridge or from the back of a truck. Or in more realistic coding examples: a CachedSousChef, a RetryingSousChef or a SousChefPool. 3) You are prevented from tightening the coupling when you improve the HeadChef class. Supplier<Ingredient> simply does not implement hitWithWoodenSpoon(). It is possible to add hitWithWoodenSpoon() to Supplier, but unlikely you'd do so. You are somewhat forced to come up with a better solution: instantiate a SelfHittingSousChef class and inject that into the HeadChef.