Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Many has been written about dependency injection, even Unity folks wrote about it long time ago, and there are some good frameworks like Zenject, so what is so cool about dependency injection?
This blog entry was first posted on my personal blog, where you can read it with the original formatting and text.
Many has been written about dependency injection, even Unity folks wrote about it long time ago, and there are some good frameworks like Zenject, so what is so cool about dependency injection? Let’s start by explaining briefly what it is and what can you use it for.
You can go to the Wikipedia entry and find a complex definition, but in plain words, it’s just a way for you class to reference other classes. So instead of creating instances of others classes directly or use GetComponent to obtain references to them, we receive them from outside, usually through the constructor in plain classes or using the SerializeField attribute.
Yes, you read it correctly, just by using SerializeField to obtain your references to other MonoBehaviour, you are already using dependency injection.
This is how your code might look like:
public class OldWayInUnity { private OtherClass other; void Awake() { other = GetComponent<OtherClass>(); } } public class OldWayInPlainClass { private readonly OtherClass other; public OldWayInPlainClass() { other = new OtherClass(); } }
And how it might look with dependency injection:
public class WithDependencyInjectionInUnity { [SerializeField] private OtherClass other; } public class WithDependencyInjection { private readonly OtherClass other; public WithDependencyInjection(OtherClass other) { this.other = other; } }
Not so much change, right? So what’s so important about it? It’s almost mandatory for the dependency inversion principle. If you are familiar with SOLID principles, you already know it, but for those who don’t know it, basically it’s a way to decouple classes using interfaces. Instead of a direct dependency on another class, you use or create an interface that the other class implements, breaking the coupling between the two.
I won’t go much more into it, there is plenty of information on this principle and the other SOLID principles and why are so important, but if you ever find yourself trying to reuse a class and giving up because it depends on so many other classes or if you are afraid of making changes to a class because too many others depend on it, the dependency inversion principle is your friend.
Decoupling is great, but decoupling just for the sake of decoupling is not enough, you need good reasons to change your mind and start using dependency injection. Here I’ll detail some examples of things that you can do that are possible or improved by using dependency injection.
Unit testing, for those are not familiar with that term, consist of writing small pieces of code that test if a certain class, function or similar complies with certain parameters. Unity offers it’s own framework (based on NUnit) to write and run tests, both in editor and in runtime. These test allow us to be sure that your code does what it’s supposed to and, what it’s more important, stay sure along time that it’s still ok, just by running your tests periodically.
You don’t need dependency injection for unit testing, but if you ever struggled to load or instantiate a MonoBehaviour in a test because it’s dependencies are too complex, then you’ll be very interested in replacing those complex dependencies with mocks. If you are wondering, mocks are objects that are only there to provide some placeholder functionality for the test. There are some good mock frameworks that make creating these mocks really easy, for example, my personal choice is Moq.
This is something that you can do thanks to dependency injection, which allows us to inject those dependencies from outside, as opposed to the old way to obtain those from within your class.
See how you could be testing objects with complex dependencies:
[UnityTest] public IEnumerator ComplexDependenciesMocked() { var objectWithComplexDependencies = PrefabUtility.InstantiatePrefab(prefabReference); var complexDependency = new Mock<IComplexDependency>().Setup(foo => foo.DoSomething("Complex")).Returns(true); Inject(objectWithComplexDependencies, complexDependency); // some Asserts follows }
Singletons are a controversial topic, sometimes hated but sometimes needed. One big critic to singletons is their implementation pattern using static members, which creates a strong coupling to that class, making it hard to test and decouple.
public class ControversialSingleton { public static ControversialSingleton Instance { get; } => new ControversialSingleton(); }
This is something that dependency injection can solve, just with the fact that you are injecting dependencies from outside and thus you control the lifetime of the dependencies from outside, your class doesn’t need to worry whether any dependency is a singleton or not. In fact, with dependency injection a singleton just means injecting the same instance to whoever request that dependency.
Avoiding the static implementation and treating it like any other class means that singletons are no longer a coupling or testing problem, so that’s a win for dependency injection.
If you are working in a team, you are probably splitting you project into pieces so each one can take care of each part, so you probably have several scenes and prefabs. One issue that arises from this splitting is how to playtest those scenes and prefabs without playing through your entire app, which is usually solved by adding some test objects in the scenes or prefabs that are disabled/removed when playing properly. There is nothing wrong with this approach, it works, but sometimes it can be prone to errors through forgotten test objects, or it requires manually creating test scenes for prefabs.
You can do it better thanks to dependency injection, because each of those scenes and prefabs have classes that declares their dependencies, you can write an editor script that loads them before entering playmode. Or even better, you can abuse the test framework and write a test that loads them (or mocks them) and stays there for a few minutes so you can play with our scene or prefab. This is quite useful for prefabs, you write long-running tests instead of manually create scenes.
Abusing the test framework to do long running editor tests looks like this:
[UnityTest] public IEnumerator LongRunningTestForPrefab() { PrefabUtility.InstantiatePrefab(prefabReference); yield return new EnterPlayMode(); yield return new WaitForSeconds(60*5); yield return new ExitPlayMode(); }
Dependency injection allows to write better code, that follows SOLID principles, more decoupled, with some complex architectures (e.g. onion architecture), test every part of it and playtest it easily.
But it comes with a price, you have to write more code for all those interfaces, you find out that using SerializeField or GetComponent might be troublesome or even worse and you have to manually assign serialized fields in the scene or pass them in the constructor. This can be quite annoying and you can start thinking it’s not worth, but this is where the dependency injection frameworks come to help.
Dependency injection frameworks take care of all the dependencies and automatically injects them wherever they’re requested, which alleviates most of the extra code and provides with some extra goods, like the lifetime control we talked about in the singletons part. There are plenty of dependency injection frameworks, some very popular like Zenject, each one with it’s pros and cons, but all looks similar, everyone have a register dependencies step and a resolve dependencies and inject step.
Instead of looking into how one works, the best way to learn about them is to write one. And to make it easier to learn, instead of going all in with all the features we want in a dependency injection framework, we’ll start building it progressively, from the simplest possible implementation to all the complexity we might want.
The first step is to register all the dependencies, usually in a collection (or several ones) where you store the dependency with it’s desired lifetime. For the sake of simplicity, we’ll only consider two lifetimes, either singleton or not singleton (same instance vs a new instance every time is request).
Let’s see how it may look:
public struct Dependency { public Type Type { get; set; } public bool IsSingleton { get; set; } } public class DependenciesCollection { private List<Dependency> dependencies = new List<Dependency>(); public void Add(Dependency dependency) => dependencies.Add(dependency); }
That looks promising, but MonoBehaviours can’t be instantiated like regular classes, so that Type won’t be enough. We can change our Dependency to follow the factory pattern but using C# Func to make it generic enough for our purposes. We’ll still need the Type though, as that is what the classes will request for injection.
public struct Dependency { public Type Type { get; set; } public Func<object> Factory { get; set; } public bool IsSingleton { get; set; } }
This is more flexible, a function that returns an object can be a call to GameObject.Instantiate or Resources.Load, we can have different ways to create our objects, but how can we obtain a reference to a prefab, for example? We need to fill our collection from something that Unity can understand, a MonoBehaviour in the scene, a ScriptableObject in the project folder, a file in a Resources folder, we have plenty of options, each one with pros and cons.
For the moment, we’ll opt for a MonoBehaviour in the scene, which is the simplest choice. We’ll name it dependencies context, but it can have other names depending on the framework, e.g. container, bootstrap or startup, to name a few.
public class ExampleDependenciesContext : MonoBehaviour { [SerializeField] private ExampleDependencyMonoBehaviour exampleDependency = default; }
Now that we have the reference, we need to put it in our dependencies collection, and we need this collection to be accessible not only by our dependencies context, but also by any other class that will need to resolve dependencies. There are several solutions for this problem, but I’ll opt for the simplest one, a static property in a static class, and I’ll name this class DependenciesContext as it is the one place all of our dependencies contexts (if we have more than one) will refer to register their dependencies.
I know, I know, static things are the sworn enemies of tests, but in this case it’s innocuous because it doesn’t have any code that needs testing or mocking, it’s just one line of code, it basically does nothing of itself and it only acts as a container of dependencies, so it’s ok to be coupled with this one.
public static class DependenciesContext { public static DependenciesCollection Dependencies { get; } = new DependenciesCollection(); }
With this class is now clear how to access our dependencies collection and we can complete our ExampleDependenciesContext class.
public class ExampleDependenciesContext : MonoBehaviour { [SerializeField] private ExampleDependencyMonoBehaviour exampleDependency = default; private void Awake() { DependenciesContext.Dependencies.Add(new Dependency { Type = typeof(ExampleDependencyMonoBehaviour), Factory = () => Instantiate(exampleDependency).GetComponent<ExampleDependencyMonoBehaviour>(), IsSingleton = true }); } }
And this is it, we can now store all of our dependencies in a single collection, but how can we obtain these dependencies from other class, that is, how can we inject them?
Imagine we have a MonoBehaviour which depends on another MonoBehaviour. Usually we’ll use GetComponent to obtain it if it were in the same GameObject or [SerializeField] to manually drag it from the scene, but both require knowing where this dependency MonoBehaviour is, and some manual scene work wherever our dependant MonoBehaviour is.
public class ExampleDependant : MonoBehaviour { private ExampleDependencyMonoBehaviour dependency = null; }
We can do better with our dependency context, which already contains all of our dependencies and we only need to care once about where they are. We just need to add a function to our DependenciesCollection to retrieve those dependencies. We just need to call the Dependency.Factory delegate to create the dependencies and cache our singletons to make sure we return the same instance every time. We can also add a generic variant for some nice syntax sugar.
public class DependenciesCollection { private Dictionary<Type, object> singletons = new Dictionary<Type, object>(); public object Get(Type type) { if (!dependencies.ContainsKey(type)) { throw new ArgumentException("Type is not a dependency: " + type.FullName); } var dependency = dependencies[type]; if (dependency.IsSingleton) { if (!singletons.ContainsKey(type)) { singletons.Add(type, dependency.Factory()); } return singletons[type]; } else { return dependency.Factory(); } } public T Get<T>() { return (T)Get(typeof(T)); } }
So now we just add some initialization code to our dependant MonoBehaviour and we can have all of our dependencies obtained from outside without caring about where they are, or even if they are singletons or not.
public class ExampleDependant : MonoBehaviour { private ExampleDependencyMonoBehaviour dependency = null; private void Awake() { dependency = DependenciesContext.Dependencies.Get<ExampleDependencyMonoBehaviour>(); } }
We have demystified dependency injection with some sample code which I hope is simple enough to understand yet useful enough to start using it in your existing code if you want to. You can see all the code from this article along with some sample usage in my GitHub repository called SimpleDependencyInjectionV1.
This injection method is not ideal, we still need to write code to get our dependencies in each class and we can have initialization issues if we don’t make sure that our dependencies context runs before any dependency retrieval, but we’ll continue to improve it in the part 2 of dependency injection on unity (coming soon).
You can find more articles like this one or similar topics in my personal blog, Modern C# in Unity.
Read more about:
BlogsYou May Also Like