Dependency injection
Dependency injection (DI) is pretty interesting. At bottom it’s a more abstract kind of encapsulation that makes a whole class more general or more single-purpose. Say I need a converter. Rather than hardcoding the data inside it, pass it in from outside. That’s still only data though. What if what you pass in is a function or a class? That’s why, if I’m going to take the dependency injection approach, I have to define an interface first. I need to define the structure of the object coming in and the output before I can do anything with it afterwards. That step alone is already fairly general and flexible. The interface can be implemented in any way at all, and you pass it in when you use it. A converter with the data hardcoded can only specialize in one particular thing, but with DI it can convert anything, as long as it implements my interface.
The typical use of this is .NET’s service injection. A controller’s initialization logic takes a service, and I don’t care where it comes from or how it’s implemented, I assume it exists and then use it. That service dependency is “passed”, or “injected”, from outside. What this comes down to is separating use from construction. How it gets constructed is your business, I assume it’s there and get on with my logic. The alternative is writing it all together, which couples the code tightly and makes the pieces hard to separate.
A step further from here is “dependency inversion”. In the previous step, what came in might still be a concrete class. But what if what I pass in isn’t a class but an interface? Then the low-level logic and the high-level logic are decoupled right there, which is why DI with dependency inversion on top is the complete form. When my injection still depends on a concrete implementing class, the level of abstraction here isn’t high enough. But if what I depend on is an abstract interface, then how the low level does it really has nothing to do with the high-level logic. The high level only cares about the structure the interface defines, and the low level can add or swap things freely while the same logic handles whatever comes. Or rather, the basis for handling whatever comes is abstraction. But the cost of abstraction is simple too. If my low level really does only have this one piece of logic and has no need to extend it, why write an extra interface and then go through the tedium of writing an implementation of it? So if there’s no reuse, there’s no need to spend extra on design. That itself is over-engineering.
There’s a question here. Why don’t I just new the class in a function’s initialization and hold it as a property of the whole class, wouldn’t that also let the whole class use it directly? Why does it have to be injected? The difference is structure versus concreteness. Do it that way and the whole codebase is tied to that one implementation. And if you run into a high-level class with a very short lifecycle, so every request news up a new service, then any cross-request data sharing inside that service is gone. The best thing to do there is make the service a singleton. And don’t forget what I said above, if you really do new the service, high level and low level are still coupled at bottom. Dependency inversion wants an interface, but are you planning to rewrite the service inside the controller? You can’t new an interface.
Dependency injection is itself an abstraction. It answers to structure, the injected-into object assumes the structure of the injected object and runs its logic from there, while new answers to a specific implementation, the flesh inside the structure included. One structure can have many concrete forms. People are all four limbs and five senses, and they still come out wildly different, but if I say it can only be one particular person, that narrows it down all at once.
A higher-order function is DI at the function level. It doesn’t care about the concrete implementation, only about the function itself, but because of its dynamic nature the structure of a function is sometimes undefined, so handling it means declaring strictly what the function looks like, with no way to pin that down strictly at compile time. In a dynamic language like Python you can only write a convention, and you can’t fix the structure at the compile level.
Translation note. I wrote this in Chinese. This English version is an LLM translation, so the wording is not mine even though the thinking is. Original: 依赖注入.