Inversion of control container in 30 minutes
Inversion of control containers feel like magic when you first come across them but they have quite simple roots. Here I’ll show you how you can evolve a basic inversion of control container with small steps from plain old direct instantiation of classes.
First let’s start with a simple app, over-engineered because I want to show nested instantiation. It relies on a PetService
that uses a PetLister
to list pet names:
You can see that the responsibility for instantiating classes is spread throughout the application. That’s simple enough to parse for this app, but in a larger app, spread across hundreds of files, it’d be much more difficult to determine exactly which classes were being used throughout. It’d also be much harder to substitute one of those classes for a test class, for example - or a mock. You have no option except changing the code and rebuilding.
Worse, if any of these types require configuration then we either have to call out at instantiation to some global source of config or pass specific config down through the call stack, unnecessarily complicating method signatures throughout.
Ideally we’d be able to gather instantiation of classes together in one place to make it easier to understand. Dependency injection and inversion of control helps there. Note that there’s nothing fancy about dependency injection: it just means instantiating a class and giving it to another class at instantiation. It doesn’t need containers or special structures. You can do it immediately, using constructor parameters, or late, using injection methods, but that’s it.
Let’s invert control over instantiation of the PetLister
in the PetService
, and accept it as a constructor parameter instead:
Immediately you have a better view of which specific classes are being used in the app. But if I want to run some tests with a TestPetLister
I still need to modify the code and rebuild:
Some argue that this is enough.
So what does an IoC container give you in addition to this? In truth, not a great deal. It hides any boilerplate involved in creating instances behind a simpler interface. It explores chains of dependencies dynamically, meaning you only need to worry about configuring single types in isolation. They also hide the boilerplate involved in using a declarative config file for instantiation, if that’s what you want. Arguably, however, you have the main benefits already.
Let’s see how that evolves from here. First, we can isolate the instantiation of these classes using a Builder
(playing a little fast and loose with pattern names), and get it out of our main method.
That’s fine, but we still need to change the code to build a test instance - or have a different Builder
for each combination of classes. Let’s do a variation on that, enabling the Builder
to support our choice between chains of dependencies. We can use a more generic interface to enable that choice (clearly just illustrative, it’s too brittle for production use):
We’re still having to hardcode the chain of dependencies. As we solve that we’ll take a step away from a simple Builder
to something more like a Container
. The insight that enables us to do so is that each class now declares the types it depends on in its constructor. By using reflection the code can dynamically determine a class’ dependencies - and by doing that recursively we can build up the whole dependency chain:
We can also add a bit of generic syntactic sugar to avoid having to cast every time we Get
a class instance:
We can now allow the user to easily choose which particular implementation of a class to use by adding a Register
method, and choosing to use a mapped, registered type if a parent class is specified. Here I’m making the Container
non-static, adding the Register
method, using registered types in Get
and then updating the main loop to use it for both live and test runs:
Now we have something that looks a lot more like an inversion of control container.
Starting from a simple layered app we saw that we could
- gather class instantiation from various positions scattered through the code to a single spot at app startup
- extract that instantiation code into a
Builder
, and support choosing between different examples of built classes - stop having to hardcode dependency chains by having the code dynamically explore and instantiate them, giving us a simple
Container