Table of Contents

Dependency Injection

When you instantiate a new object by calling the class contructor and provide a constructor argument, you have performed dependency injection. In fact, that is the ideal, and most common form of dependency injection. Constructor injection should be preferred whenever possible. Objects that instantiate other objects should provide dependencies to their child objects. Dependencies should be passed down through the application hierarchy, starting from the composition root. In a standard C# application, the composition root is the main method that runs when the executable runs, but Unity provides us no access to a composition root by default.

With dependency injection, the dependent object does not seek out its own dependency, like when using a static reference. When an object contains a direct reference to a type that it depends on, those types are tightly coupled. You cannot separate the implementation of the dependent object from the object upon which it depends. This can lead to an unnecessary amount of strong dependencies throughout your code base over time, eventually leading to spaghetti code, where all of your classes are tangled together. They can't be separated from one another without causing compilation errors. When many classes have these types of strong dependencies it can feel like every file in the project has a chain of dependencies that ultimately references every other file in the project. This makes it very difficult to separate your modules along clean conceptual boundaries.

So, what is the alternative? When you have a well defined separation of concerns between the responsibilities of various classes and modules, those modules can be loosely coupled together via abstract interfaces. An interface provides a minimal contract for object interaction, only the required methods, properties, and events, without requiring the full implementation. This allows your types to depend on simple interfaces, rather than needing the particular implementation of a certain class. When an object depends on an interface, it becomes trivial to swap the actual implementation behind that interface. And, the dependent object is more portable, as it only requires the interface. This allows code modules to be transferred more easily between projects.

Programming objects to depend on interfaces encourages S.O.L.I.D. programming principles and clean code architecture. More accurately, S.O.L.I.D. code actually requires interface injection. When a type depends on an interface, the concrete implementation of that interface must be injected in from the outside. You can't just make reference to a static interface. So, dependency injection is a fundamental and necessary component of a truly modular code base.

Singletons, Service Locators, and Static References

Unity programmers are quite familiar with using singletons, service locators, and static references. However, when one type depends on a static reference to another type, it becomes tightly coupled to it. It cannot compile without the other. So, why use static references if they always result in tight coupling? In fact, there are many who say that you should never use them. However, there are circumstances where tight coupling is acceptable, there are even circumstances where a static reference is indeed the best option.

This is the case in an environment such as Unity. In Unity, your scripts commonly derive from the MonoBehaviour class, and are instantiated automatically by the Unity runtime. By the time your MonoBehaviour methods execute, the constructor has already been called. So, constructor injection is not an option. We could use method injection, or inject values directly into properties, but without knowing the new object has been instantiated, how is injection triggered?

Most frameworks only concern themselves with injecting objects that are already saved into scene files. They usually require additional components to be added to your scenes, and may require you to load certain scenes and components first. If they even support injecting dynamically instantiated game objects, like dropping a prefab into a scene while play testing, they usually do so with a service locator. Often these service locators are bloated with dependencies that run deep into that specific framework.

The Unity Way

Unity does not allow you to directly instantiate new instances of MonoBehaviour derived scripts using a constructor. Constructors are called by the Unity runtime. There is no event invoked when a new MonoBehaviour is created. There is no way to register a factory to inject new MonoBehaviours. Therefore, your scripts have no choice but to directly reference some static dependency. Objects can be added directly to a scene from the editor while playing, and there is no way for you to inject them. The typical wisdom from outside Unity concerning dependency injection and static references is not always applicable due to these environmental differences.

The Service Locator pattern is relevant here. A service locator is a singleton or static class that provides dependencies to other objects upon request. It is a service that provides other services. This pattern comes with all of the concerns associated with any other static reference, like tightly coupling your code to that particular implementation. In most other programming environments, where constructor injection is not restricted, this is an anti-pattern to be avoided. But, when constructor injection is not available, a service locator seems unavoidable. However, when the implementation is sufficiently abstract even a service locator can work like an interface. This is the inspiration for the Injector Locator pattern.

The Injector Locator Pattern

When object instantiation is beyond your control, and constructor injection is impossible, it becomes necessary to use a static reference to satisfy certain dependencies. That is why Unity dependency injection frameworks, if they even support injecting editor insantiated objects, use a service locator of some kind to do it. However, if the service locator is made abstract, then it can function much like an interface. For that purpose, the InjectorLocator was created. It serves as a static reference with a single responsibility, to provide an IInjector interface. The IInjector has a generic method that then provides instances of any type.

One concern with static references is that they are globally accessible. Global values can be altered from anywhere, potentially corrupting the state of the application. The IInjector assigned to the InjectorLocator could be removed while the application is running. However, C# delegates provide a way of setting a global value, and keeping access private for the setter. Delegates can refer to private or protected methods, enabling classes to hide public access to a method delegate. If a global value is actually a delegate that returns the desired value, a private method delegate can be assigned to it. Global access to remove the assigned delegate can then be restricted, unless the matching delegate is provided. Therefore, even though the static value is global, if the assigned delegate is private, it cannot be removed by an external agent.

The Switchboard implementation of this pattern is free and open source under the MIT license. It is publicly available on the Switchboard GitHub repository located at https://github.com/swipetrack/switchboard/tree/main/interface/InjectorLocator. The entire Switchboard framework is designed for loose coupling. Therefore, the only real dependency you need is on this simple, open source pattern. Technically, the entire rest of the Switchboard framework can be removed from your project at any time without error.

The IInjector Interface

The IInjector interface is the fundamental interface for dependency injection. It consists of a single, generic method. The generic type argument specifies the type of object requested from the IInjector. The IInjector can return an instance of that type, but it may return null if it cannot fulfill the request. Once you have an IInjector instance, you can request any type of object from it, and it is all done via loose coupling.

Composition Root & Dependency Injector

The CompositionRoot is the entry point for initializing the application at run time. This happens when you press play in the editor, or launch the application. On the CompositionRootAsset in the Switchboard settings menu there is a Dependency Injector field. There, you can assign a DependencyInjector asset. The DependencyInjector class implements the IInjector interface, and the CompositionRoot passes it through as the result of the InjectorLocator.GetInjector() method. This allows MonoBehaviours to indirectly reference the DependencyInjector as an IInjector. They only need to directly reference the InjectorLocator.

First, the CompositionRoot loads the CompositionRootAsset. Then, it invokes Activation() on the assigned DependencyInjector. The DependencyInjector assigned to the CompositionRoot is saved when play begins, so changing the value at run time has no practical effect. In the MonoBehaviour.Awake() method, MonoBehaviours can call InjectorLocator.GetInjector() to get the DependencyInjector as an IInjector. Then, the IInjector.Get() method can return any Type of dependency from the DependencyInjector. Deactivation() is invoked when the Destructor hidden in Unity's persistent DontDestroyOnLoad scene is destroyed.

A DependencyInjector is a ScriptableObject, so it exists as an asset in your project, not in the scene hierarchy. The DependencyInjector is where you can bind assets and other objects to the root of your application. Tight coupling occurs in the DependencyInjector, to the root objects bound within it, so that everything else beyond the root can remain modular, using injected references to each other. Serialized Unity objects with the Expandable attribute can be expanded in the editor, so you don't have to select a nested object to modify its properties. Everything nested within the CompositionRoot can be modified in one place. You can instantiate pure C# classes and pass the properties assigned to the DependencyInjector as arguments to class constructors. Thus, you can compose the business logic of your application at the composition root, using plain old C# classes and constructor injection.

You can derive various implementations of DependencyInjector, and save multiple instances in the editor. The injector can be swapped out easily to completely change the root behavior of your application. You can create a mock dependency injector and swap to it for unit testing. Any properties that you change in the DependencyInjector asset are live at run time, and persist after you stop playing. This makes testing, tweaking, and experimenting simple and intuitive.

Other Frameworks

Most other frameworks rely on reflection. Reflection in C# allows you to access information, methods, and properties of a type or instance, even if they are not public. Reflection operations are relatively costly in terms of performance. Reflection should be avoided at run time, especially in the context of a video game, where performance is a top priority. Most dependency injection frameworks have you assign framework specific attributes to your class members. At some point, hopefully not at run time, the dependent objects undergo reflection. All of their members are examined for those attributes, and dependencies are injected accordingly.

If not at run time, code or content gets generated by a reflection baking process that happens earlier, to improve performance. The problem with reflection is that it is notoriously slow. The performance penalty is either added to your game at run time, or it is added to your development process in dev time. Whenever changes are made, the reflection baking process has to happen again. Relying on reflection can also result in problems caused by code stripping. Code stripping automatically removes unused code from your build, and Unity does not understand reflection based relationships in your code.

Switchboard has none of those issues. No reflection occurs at design time, compile time, or run time. It is avoided entirely, and the performance is nearly instantaneous. Switchboard is fully compatible with disabling domain reload and scene reload. You can press play and begin working nearly instantly. You can drop a prefab game object into a scene, and it will automatically get its dependencies without any effect on performance. Properties at the composition root can be updated in real time, and the changes persist when you stop play, unlike changes in the scene hierarchy, which are reverted. All dependencies can be altered with a single reference, and you're ready to play immediately.

Some solutions also struggle with execution order. Getting your composition root to load first, and persist until all MonoBehaviours have been destroyed, is a genuine problem. Many plugins have elaborate requirements for you to deal with. You may be required to add framework specific objects to your scenes or project. You may have to load a certain scene before you can load others. Switchboard has all of the execution order issues resolved for you. The CompositionRoot activates the DependencyInjector when you press play, or when starting the application. When it activates, the first scene is already loaded, but no MonoBehaviour Awake(), OnEnable(), or Start() methods have been called. You can instantiate objects, including game objects, before any other scripts execute. If a class doesn't need to exist in a scene, it can just exist at the composition root. You will be able to write more of your code as pure C# classes that activate when you play. The DependencyInjector automatically deactivates after all other MonoBehaviours have been destroyed.

Eliminating an explicit reference to another class does not necessarily mean that you have eliminated tight coupling. If your code base is littered with framework specific attributes, that is a type of dependency. Some frameworks require your MonoBehaviours to derive from a specific base class, or implement a particular interface, or add framework specific components. To inject objects that are instantiated via the editor, other frameworks also use a static service locator, just like Switchboard. This service locator may be buried in some other component that your code does not directly reference, but their service locator is still a requirement. You must attach those framework specific components to your game objects. Switchboard is the only framework that embraces the facts, and abstracts the necessary service locator into a tiny, open-source design pattern. With other frameworks, even if some of your dependencies are loosely coupled, the rest of your project still ends up tightly coupled to that framework. It really negates the overall purpose of dependency injection. When using Switchboard, you only depend on the open-source Injector Locator pattern, and you only reference it on one line per class.