Just a quick brain dump about dependency inversion, dependency injection and the “Russian dolls” style of object composition that a lot of developers ask me about.
Consider this piece of legacy code for a video rental application:
The pricing logic of our app uses IMDB ratings that it fetches from a web API. If we wanted to unit test this logic, or use a different source of video ratings, we’re stuck because the code to fetch the rating is embedded with the pricing logic.
We can extract the fetching code into its own method in its own class and inject an instance of it in the constructor of Pricer.
It’s now possible to substitute a different implementation of ImdbAPI (e.g., a stub, for unit testing) from outside Pricer.
But if we look one level up in our call stack, we see we still have a problem.
Our Rental class knows about Pricer and knows about ImdbAPI, so Rental is not unit-testable. We can fix this by injecting Pricer into Rental.
This is the “Russian dolls” style of composition I mentioned at the beginning. ImdbAPI is injected into Pricer, and Pricer is injected into Rental. So we swap the pricing logic without changing Rental, and we can swap the ratings source without changing Pricer, and every object in the chain only knows about the next object in the chain – and only its interface (method signatures). Notice how the object at the bottom of the call stack is created first.
Notice now that our highest-level module, Program.py, is “wearing” those dependencies. Program knows which implementations for pricing and fetching movie ratings are being used. Our core internal logic knows none of these details.
This is a natural consequence of dependency inversion and the “Russian dolls” style of composition – effective modular systems wear their dependencies on the outside.
The eagle-eyed among you will have noticed that Pricer has one concrete dependency, on Video.
But you should also notice that, while Pricer creates a Video, it doesn’t use the Video. This is very important: objects that reference other objects’s implementations shouldn’t call their methods, and objects that call other objects’ methods shouldn’t reference their implementations.
That’s a more essential form of dependency inversion. Any class that uses a Video‘s methods shouldn’t bind directly to an implementation. So we could stub Pricer to return any object that looks like a Video from the outside. This is only possible if Pricer is swappable.