Of houses, wires and dependency injection

In the last post, I’ve written a little rant on how awful MonoBehaviour initialization is and teased little more advanced frameworks that can help with that. Specifically - I’m talking about dependency injection.


Before going any further, let’s quickly describe the issue we’re trying to solve. When creating an object, we want to be able to say whether it already exists or not. Wait, what? Well, let's say we’re building a house. We laid down a foundation, raised some walls and called it a day. Is a house built yet? Can I move in? No - the house is being built, but is not ready yet - not until we have added a roof, doors, windows and probably some media like electricity. It needs all those things to be considered existing.


Now, back to C#. When creating an object, there’s a simple way to ask whether it exists - it’s to compare it against null. There is also, in fact, a way to know when an object is being built - it is, when we're inside the constructor.


Ok, but what does that knowledge give us? In our house example, there are certain things we're safe to expect from a house that exists. For example, electricity - it is safe to assume not only that there will be some power outlets, but also - that those outlets actually provide energy (so the house was connected to the local network.) The house has been completely built and now it does everything we would assume the house to do in general. The house has been properly initialized - it depends on electricity and this dependency has been provided to the house.


The analogy stands in C# too - once the object has been built, it should already have all the dependencies that it needs to function. Let's say we have some GameScenarioManager in an RPG game - when we encounter enemies, music changes from ambient to dynamic. Code responsible for changing the music lies in BackgroundMusicManager.SwitchTheme function - so GameScenarioManager depends on BackgroundMusicManager.


But where does electricity in the house come from? Well, we put it there while building the house (the wires providing it, that is). That's why it's important to know when the object is being built - since this is when we want to provide it with dependencies. So, in C# we do it in constructor code - in the GameScenarioManager, we add a constructor with a parameter of type BackgroundMusicManager and save it locally. From now on, whenever we know that music should be changed, GameScenarioManager notifies BackgroundMusicManager which handles the rest of the details. Providing the code with stuff that it needs from the outside is called dependency inversion/injection (DI) or inversion of control (IoC).


See any problem with that? Yeah, exactly, MonoBehaviour does not allow us to call a constructor. Who would have thought that Unity will make it harder, it never happens, right? The easiest way to solve it would be to call FindObjectOfType in Awake... The problem is, we enter the realm of Script Execution Order Hell this way. Let's say there is a scene starting in combat: some other class - in its own Awake - finds GameScenarioManager and forces it to change game mood, including music. We try to do so and bang!, MissingReferenceException. GameScenarioManager is not null, but wasn't fully initialised, because its Awake didn't run yet. We thought we had a house, but we're left with a well constructed, I don't know, hut? Since there's no electricity, house doesn't really exist, even though it's != null!


Fortunately, there exist IoC containers for Unity - and the one I'm going to talk about is Zenject. Not the only one out there, but hey - it's a start!


So, the Zenject way of solving this would be to create an Installer. It'll contain definitions of all the types that our game uses and the way they interact with each other (I simplify a lot here, Zenject documentation is well written, so if you are interested - it's a good read). Among other things, this Installer will have a code like:

[SerializeField] private BackgroundMusicManager _musicManager;
...
Container.Bind().FromInstance(_musicManager).AsSingle();

What is going on there? We provide our music manager to the installer using Unity serialization. If it wasn't Mono, we could also create it using new or rely on Zenject to create it for us. The important thing is - we have a dependency that we want to inject. We hold the electric wire in our hands - now all we need to do is shove it into the wall and, well, wire it up.


By calling Container.Bind(), we define that BackgroundMusicManager is a dependency - it might get injected into classes. FromInstance(_musicManager) tells Zenject to use the specific object we have serialized when injecting. AsSingle() clarifies that this will be the only instance of BackgroundMusicManager declared as dependency.


The last thing that needs to be done is providing a method in GameScenarioManager that will receive the dependency:

private BackgroundMusicManager _musicManager;
…
[Inject]
private void Init(BackgroundMusicManager musicManager)
{
    _musicManager = musicManager;
}

And that's it! Now, when we initialize the scene, we can be sure that everything is ready to use.


But hey!, you may think. There are so many different ways to do that without all that hussle! Well, yes there are - but I'll try to explain why I think using an IoC container is usually the best choice:

  1. So, we could just put the dependency right into the dependent class using serialization. This would work under two conditions: project is small scale and both objects are MonoBehaviour. In the real world, projects don't stay small scale for long and I already wrote why I think MonoBehaviour is evil.
  2. You could create a dependency inside the dependent class. This will work as long as the dependency is unique - the moment you try to share dependencies this way, you are out of luck.
  3. You could use a Service Locator pattern - except, it's actually considered an antipattern. In short, Service Locator is an object - possibly, but not necessarily, a Big Static Singleton - that can provide you with every dependency you might ever need. Improperly used IoC container will devolve into Service Locator. This requires some more explanation though: typically, when using IoC, there will be logical phases of installation and usage. The only time you are allowed to touch Container is during the installation phase - this way, you might be certain that the stuff provided by DI is consistent. Service Locator tempts you to break that rule - but it's usually a wrong idea. But hey, we're in the Internet - feel free to disagree!


So, is dependency injection a silver bullet that will solve your initialization issues always forever? Nope. Will it improve your code structure? Yep. Is Unity making it harder than it should be? Yep.


But the most important question - is a house with no electricity an actual house, or is it just a glorified hut - still remains unanswered.

Back