The journey from global variables to managed dependency injection
When writing object-oriented code, we inevitably end up with some objects that are either unique (we only want one instance of them), long-lived (they stick around for the whole time the program runs), or referenced from many different places. In these cases it's tempting to use single global instances for these objects.
The problems with global instances
Unfortunately, global instances contribute to making code painful to understand and difficult to change. When we use global instances, we hide our object interactions deep in the details of the code: any instance might be using any other instance, and the only way to tell is to read through every single line of code that is called.
Using global instances also does nothing to encourage us to separate discrete behaviour into discrete places. When we refer to global instances we don't need to think about which classes should depend on and use which other classes, we just grab whichever global we want at a moment's notice here and there.
The benefits of dependency injection
Dependency injection is an alternative to using global instances. Injecting dependencies simply means passing instances in as parameters to the constructors of the classes that use them. This improves code readability, allowing us to see a class's dependencies with a quick glance at the class constructor.
Injecting dependencies encourages us to think carefully about where we partition behaviour in our classes because we feel the pain every time we inject a new class. This pain makes us work hard to group behaviour into discrete places and to have classes only use other classes that they really require. This all adds up to a reduction in coupling between classes and a more cohesive design.
Another benefit of injecting dependencies is that it makes testing our classes much easier. We can test class instances in complete isolation from each other because we can easily inject test doubles. This is especially useful if one of our instances does something that takes a long time, or costs money every time it happens—for example, making a network call to a paid web service. Anything that depends on such an instance can be injected with a test double to avoid making a real network call.
A drawback of dependency injection
Dependency injection does come with some drawbacks. We may end up with class constructors that require two, three, or more different dependencies. For example, we might have an App
class that needs to be injected with an instance to manage I/O, an instance to manage network requests, and an instance to interface with a database. Each of these instances may also be injected with other dependencies, all of which must be correctly coordinated.
It's laborious and error prone to update class constructors with new dependencies every time we refactor or add code. We may have to pass an injected instance down through multiple layers of code to the place where it is actually used. It's error prone to rely on yourself to always remember to correctly update these parts of the code. This is doubly true for weakly typed languages.
We can use an inversion of control container to solve this problem. The inversion of control container can construct an instance of a class according to that class's own constructor definition. We typically call the container asking it to construct our top level instance. It will then recursively construct instances of the correct classes needed by the top level constructor.
Understanding Inversion of Control Containers
Dependency injection and inversion of control are two related but distinct concepts. Dependency injection is about writing classes in such a way that a particular instance of a class or function can be passed in to them at construction time.
Inversion of control is the general principle of relinquishing control from where it normally lies. If a client instance normally calls a Ui
instance asking for user input, inversion of control would be having our client instance instead provide methods to let the Ui
instance call it when the Ui
receives user input.
Inversion of control is the principal difference between a library and a framework. When using a library, we call specific functions and methods to perform calculations and operations, but we put everything together ourselves. When using a framework on the other hand, we register different bits of our code with the framework and then we let it do its job.
An example of this would be using a frontend web framework. If we use something like React router, we tell the framework: "these are the URL paths, these are the views that should render on those paths." Then, instead of micromanaging browser events and rendering specific pages, we leave it up to the framework to do its thing. It calls our code, we don't call its code.
Dependency injection implies an inversion of control because our objects no longer locate other objects that they need when we call methods on them. Instead, they are given dependencies at construction time and simply trust what they are given to do the right thing. However, while the class "trusts" us to construct it correctly, we must still manage the construction.
We can take dependency injection a step further by using an inversion of control container. With an inversion of control container, we simply register all our available classes up front. When the container needs to construct an instance of a class, it can check what objects the class's constructor requires, and it can then construct the appropriate instances from among the classes registered with it.
House parties and event planners
I think a useful metaphor for understanding all this is the metaphor of organising a party. The simplest case would be not using dependency injection. We could think of this as organising a house party for a small number of people. I would invite people myself, buy ingredients and prepare food myself, and welcome and entertain people on the night. Even if I buy pre-prepared food at the supermarket, I'm still locating it and laying it out myself: I have a lot of knowledge of how it's provisioned and the exact food I'm providing. This could be compared to writing a very basic program all in one file with just a few classes and no dependency injection. Our classes all know about the specific global instances of each other.
But what if we had a more elaborate party? We could instead inject our dependencies—in this metaphor, if I was hosting 100 people, I could organise caterers to provide the food instead of doing it myself. The caterers would be like a class whose interface was to provide food for an event. If these caterers were seen as an injected dependency, I would know that I could swap the caterers out for different caterers who provide different types of food or services. I could "inject" different caterers in different scenarios with different requirements. I don't have to know anything about the exact food that will be provided if I don't want to. Likewise, instead of personally inviting everyone and personally taking everyone's coats, for a really big party I could outsource or "inject" all these parts of the party by using external services.
So we've seen how dependency injection differs from doing everything ourselves, but what does inversion of control look like in this metaphor? Proper inversion of control would be hiring an event planner. I would relinquish all control to them. I would rely on them to know what is necessary for a successful party, and to source those things, put them together, and coordinate them. I could just tell them: "here is the type of party I want to have, these are the attendees, these are the possible dates." All I need to do is to give a few configuration parameters and then tell them to organise it. I shouldn't need to call them to micromanage which caterers they are using, or that they've checked the RSVPs. Instead, they would handle any minor issues, and just inform me when the event is ready.
An inversion of control container is like the event planner. It manages all the different requirements for creating a specific object instance. It allows us to move away from a system in which we have to manually initialise and inject all the different pieces of the application.
Rolling our own Inversion of Control Container in Java using the Reflection API
So now that we know what the point of an inversion of control container is, I think it will be helpful to see what a basic implementation of one in Java looks like under the hood. Let's look at what we're aiming for.
The situation we would have without an inversion of control container is that we first manually instantiate all our dependent classes, then we pass them in to the App
constructor in the correct order. (The class names I've used below are just example names.)
Router router = new Router();
HttpGetRequester getRequester = new HttpGetRequester();
Ui ui = new Ui();
App app = new App(router, getRequester, ui);
With an inversion of control container, we can instead register all our dependent class types and then tell the container to construct the App
class using its registered types. We don't have to worry about the ordering. If we change the dependencies or make a class depend on a different class, we don't have to change line 5
of the code in which the App
is constructed by the container.
container.registerType(Router.class);
container.registerType(HttpGetRequester.class);
container.registerType(Ui.class);
App app = container.construct(App.class);
In order to implement the container, we make use of Java's Reflection API. The Reflection API allows a Java program to examine aspects of its own structure, including classes and functions. It will allow us to ask a class for its constructor functions, and to query those constructors to find out the types of objects that they take as parameters when being invoked.
So let's look at some code to see what doing this actually looks like. I'm going to walk through the container's construct
method step by step.
public <T> T construct(Class<T> clazz) { ... }
The use of Class
in this context is a bit confusing. It is actually a reference to a class named Class
, which represents and gives us access to methods to examine Java classes. The type Class
uses the generic <T>
parameter to refer to the class that it represents. For example, a class
instance representing the App
class would have a type of Class<App>
.
What this line means is that our container defines a construct
method, which takes a class
instance of generic type Class<T>
, and returns an instance of the type T
. The method uses the generic type <T>
to encode the fact that it can return an instance of any type, but that the type of the instance it returns will always be of the type that the passed-in Class
represents. Also note that we give the Class
argument the name clazz
because class
is a keyword in Java.
Constructor[] constructors = clazz.getConstructors();
The first line of our method uses the getConstructors
method to get a list of Constructor
-type instances for the class to be constructed. These constructor
instances are representations of the actual constructor functions available on the passed-in class.
Constructor<T> constructor = (Constructor<T>)constructors[0];
We next extract the first Constructor
instance from the list and cast it using (Constructor<T>)
so that it is treated as an instance of type Constructor
parameterised with the type of the class that we're constructing. I am assuming that the first constructor for the class is the one we want to use because I'm writing a simplified container class.
Class[] parameterTypes = constructor.getParameterTypes();
Now we need to find out the types of the parameters that we need in order to actually invoke the constructor. We call the constructor
instance's getParameterTypes
method, returning a further list of Class
objects. These Class
objects represent the types of the parameters required by the constructor.
Object[] parameters = makeParameters(parameterTypes);
Now that we have the parameter types, we make a call to the method makeParameters
, which I defined on the Container
class. This method takes the list of Class
instances and returns a corresponding list of actual instances of the correct class types. It does this by recursively looking through the Container
's list of registered classes to find and construct the objects that are required.
To give a concrete example, in this step if Class[] parameterTypes
contained instances of type [Class<App>, Class<Ui]
, the new Object[] parameters
would contain instances of type [App, Ui]
.
If you're interested in seeing the full Container
class code to look over how I stored the registered classes and how I initialise them in makeParameters
, you can see the full code on GitHub.
return constructor.newInstance(parameters);
Finally, we actually construct and return an instance of our class. We construct it by calling the newInstance
method on the constructor
instance that we retrieved earlier. The newInstance
method takes an Object[]
as its single argument. Here we use the array parameters
that we created in the previous step. This method simply calls the class's constructor to construct an instance of the class using these parameters.
So altogether the code for our construct method looks like this:
1 public <T> T construct(Class<T> clazz) {
2 Constructor[] constructors = clazz.getConstructors();
3 Constructor<T> constructor = (Constructor<T>)constructors[0];
4 Class[] parameterTypes = constructor.getParameterTypes();
5 Object[] parameters = makeParameters(parameterTypes);
6 return constructor.newInstance(parameters);
7 }
It takes a class to be constructed, retrieves the class's constructor, locates instances of each parameter type that the constructor requires, and invokes the constructor.
This is pretty much everything for our basic inversion of control container construct
method.
Recap
Dependency injection is a useful way of organising code such that classes can be less micromanaging of other classes, and such that the dependencies between different objects in our code are explicitly referenced. In doing this, dependencies become more traceable and more readable, and we don't create global state.
We can inject dependencies by simply passing them in to our class at construction time. This can become cumbersome when we have a lot of different classes with different dependencies. An Inversion of Control container helps solve this problem and make it less error-prone by automating it.
Inversion of control is actually pretty much exactly what it says on the tin: turning the control relationship on its head.
Full code on GitHub: https://github.com/onlyskin/java-ioc