To fully understand what benefits Init(args) can unlock, one needs to first understand a key principle in software engineering: inversion of control.
What inversion of control means in a nutshell is that instead of classes independently retrieving specific objects to work with, they will work with what ever objects are provided for them by other classes. This comes with many benefits such as the ability to easily switch the objects passed to instances with different ones.
Usually one can achieve inversion of control by simply providing the objects that a class depends on in their constructor:
using UnityEngine; public class Player { public IInputManager InputManager { get; } public Camera Camera { get; } public Player(IInputManager inputManager, Camera camera) { InputManager = inputManager; Camera = camera; } }
Unity’s MonoBehaviours or ScriptableObjects however can’t receive any arguments in this manner in their constructor, making it more difficult to use inversion of control.
This tends to lead to a situation where methods such as the Singleton pattern are used all over the place, tightly coupling classes with other classes in a tangled web of dependencies.
using UnityEngine; public class Player : MonoBehaviour { private void Update() { if(InputManager.Instance.Input.y > 0f) { float speed = 0.2f; float distance = Time.deltaTime * speed; transform.Translate(Camera.main.transform.forward * distance) } } }
While this does accomplish the job of retrieving the instance, it also comes with some pretty severe negative side effects that may end up hurting you in the long run, especially in larger projects:
- It can cause the dependencies of a class to be hidden, scattered around the body of the class, instead of all of them being neatly defined in one centralized place and tied to the creation of the object. This can leave you guessing about what prerequisitives need to be met before all the methods of a class can be safely called. So while you can always use GameObject.AddComponent<Player>() to create an instance of a Player, it’s not apparent that an InputManager component and a main camera might also need to exist somewhere in the scene for things to work. This hidden web of dependencies can result in order of execution related bugs popping up as your project increases in size.
- It tends to make it close to impossible to write reliable unit tests. If the Player class depends on the InputManager class in specific, you can’t swap it with a simple mock implementation that you’d be able to easily control during testing.
- Tightly coupling with specific classes can make it a major pain to refactor your code later. For example let’s say you wanted to switch all classes in your code base from using the old InputManager to a different one like NewInputManager; you would need to go modify all classes that referenced the old class, which could potentially mean changing code in hundreds of classes. In contrast when using inversion of control, you might be able accomplish the same thing by changing a single line of code in your composition root (the place where the InputManager instance is created), and from there the new IInputManager gets forwarded to all other classes.
- Tight coupling with specific classes also means less potential for modularity. For example you can’t as easily swap all your classes from using MobileInputManager on mobile platforms and PCInputManager on PC platforms. This limitation can lead to having bulky classes that handle a bunch of stuff instead of having lean modular classes that you can swap to fit the current situation.
- Tight coupling can also make it impossible to move classes from one project to another. Let’s say you start working on a new game and want to copy over the Camera system you spent many months perfecting in your previous project. Well if your CameraController class references three other specific classes, and they all reference three other specific classes and so forth, that might have no choice but to start over from scratch.
The reason why Init(args) exists is to avoid all of these issues, by making it very easy to achieve inversion of control in a way that feels native to Unity.
If the Player class derives from the new generic MonoBehaviour base class, it will receive the inputManager and camera arguments in its Init function, where you’ll be able to assign them to instance variables:
using UnityEngine; using Sisus.Init; public class Player : MonoBehaviour<IInputManager, Camera> { public IInputManager InputManager { get; private set; } public Camera Camera { get; private set; } protected override void Init(IInputManager inputManager, Camera camera) { InputManager = inputManager; Camera = camera; } }