2. Why Init(args)?

  01. Introduction No Comments

To fully grasp the advantages that Init(args) can unlock, it’s essential to first understand a key design principle in software engineering: inversion of control.

In a nutshell, inversion of control means that instead of classes independently retrieving the objects they need, those objects are provided to them by other classes.

This approach comes with several benefits, such as making it very easy to swap out delivered objects with different ones.

Typically, inversion of control can be achieved by passing the objects that a client needs to it via its constructor:

using UnityEngine;

class Player
{
    IInputManager InputManager;
    Camera Camera;

    public Player(IInputManager inputManager, Camera camera)
    {
        this.inputManager = inputManager;
        this.camera = camera;
    }
}

However, Unity’s MonoBehaviour or ScriptableObject classes don’t support constructor arguments, making it challenging to apply inversion of control.

This situation often leads developers to rely on things like the Singleton pattern instead, leading to classes being rigidly bound together in a tangled web of dependencies.

using UnityEngine;

class Player : MonoBehaviour
{
    void Update()
    {
        if(InputManager.Instance.Input.y > 0f)
        {
            var speed = 0.2f;
            var 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.

🔍 Hidden Dependencies

Dependencies are scattered all over the class rather than being clearly and statically defined in one place. This obscures what requirements must be met for the component to function correctly.

For example, while you can instantiate the above Player at any time using GameObject.AddComponent(), or attach it to any GameObject using the Inspector, there’s no way for you to know in either case that an InputManager and a main camera must also exist somewhere in the scene for it to actually work!

🧪 Testability

It tends to make it near impossible to write unit tests. If the Player class depends on a specific InputManager singleton object, you can’t swap it with a simpler test double during tests, nor modify it its state in any way without the potential for undesired side effects.

🔧 Maintainability

If you need to change a service to a different one – say, InputManager to NewInputSystemManager – you’ll need to update every class that uses the old one.

🧩 Adaptability

You lose the flexibility to swap implementations based on context, such as using MobileInputManageron mobile platforms and PCInputManager on desktop platforms.

🚫Reusability

Tightly coupled systems are harder to reuse across projects. For instance, if your CameraController depends on several other specific classes, which all depend on several other specific classes, you won’t be able to migrate it to another project without a lot of rewriting.

Enter Init(args)

Init(args) addresses all of these issues by making it trivial to achieve inversion of control in Unity, with the introduction of the new Init function:

using Sisus.Init;

class Player : MonoBehaviour<IInputManager, Camera>
{
    IInputManager inputManager;
    Camera camera;

    protected override void Init(IInputManager inputManager, Camera camera)
    {
            this.inputManager = inputManager;
            this.camera = camera;
    }
}

This makes all dependencies explicit, and makes the code testable, modular and maintainable by default.

Leave a Reply

Your email address will not be published. Required fields are marked *