Dependency Injection

Introduction

We should start from simple question. What is a dependency injection and how we can implement this concept 🙂 ? Let’s start from explaining what is a dependency itself. When a project grows, it has more and more objects. They need to communicate and interact with each other. In other words we can say that the object A depends on the object B if A need to interact with B. This is a dependency.

Let’s imagine that we have simple scene with three elements: character, camera and user interface.

Image 1. Simple scene with character and user interface allowing to change some parameters.

User interface allows to rotate character and change camera configuration. There are a few potential dependencies between objects:

  • user interface depends on character – needs to display current rotation and update character when user provides new value,
  • user interface depends on camera – needs to display current setup and update it when user provides new values,
  • camera depends on character – should be able to follow character’s position.

How can we setup those dependencies to make our project working? Unity allow us to set references to game objects or assets in the inspector if a class has serialized fields. It would be enough in our simple example, but is this approach scalable to use in larger project? Unfortunately, it has some limitations and will be cumbersome to use. For example we won’t be able to setup references to non-serializable objects. Second issue is related with objects created dynamically in runtime. Such game objects can be gathered using Find/FindObjectOfType, but those methods are expensive in terms of performance.

Dependency Injection

Our problems can be solved by dependency injection. It’s a pattern describing how to deliver a dependency to an object which needs it. There are multiple ways how to implement the pattern. The simplest one is just injecting dependencies through constructor or initialization method.

Unfortunately, we will need to update them each time when new dependency is added. Additionally, we have to gather all needed references somehow, before we will be able to pass them to newly created object.

It would be good to have more automized way of managing dependencies. For this purpose we can create simple dependency injection container. It will serve kind of as a bag where we can put all our objects and later get a reference whenever we need it.

public class DIContainer
{
    protected Dictionary<Type, object> dictionary = new Dictionary<Type, object>();

    public void Register(object objToRegister, Type objType = null)
    {
        objType = objType == null ? objToRegister.GetType() : objType;
        if (!dictionary.ContainsKey(objType))
        {
            dictionary.Add(objType, objToRegister);
        }
        else
        {
            Debug.LogError(string.Format("[DIContainer] dependencies conflict, object with specific type ({0}) is already registered", objType.ToString()));
        }
    }

    public void Unregister(Type objType)
    {
        if (dictionary.ContainsKey(objType))
        {
            dictionary.Remove(objType);
        }
    }

    public T1 GetReference<T1>()
    {
        var foundObj = FindDependency(typeof(T1));
        return (T1)foundObj;
    }

    private object FindDependency(Type type)
    {
        if (dictionary.ContainsKey(type)) return dictionary[type];
        else return null;
    }
}

This simple implementation has three methods:

  • Register – allows to add new object to container,
  • Unregister – allows to remove an object from container,
  • GetReference<T1> – gets reference to an object with specific type if it was registered in container.

Now it would be enough to provide DIConatiner to our objects and they will be able to get dependencies through it. Let’s change container to MonoBehaviour and make a Singleton to simplify. We won’t need to distribute references to DIContainer. Objects will be able to get it through public static access.

public class DIContainer : MonoBehaviour
{
    private static DIContainer cachedInstance = null;
    public static DIContainer Instance
    {
        get
        {
            if (cachedInstance == null)
            {
                var newObject = new GameObject("DIContainer");
                cachedInstance = newObject.AddComponent<DIContainer>();
            }
            return cachedInstance;
        }
        private set
        {
            cachedInstance = value;
        }
    }

    #region MONO BEHAVIOUR

    private void Awake()
    {
        if (cachedInstance != null)
        {
            if (cachedInstance != this) Destroy(gameObject);
        }
        else
        {
            cachedInstance = this;
            DontDestroyOnLoad(gameObject);
        }
    }

    #endregion

    ...
}

Can we somehow improve current implementation? Even with simple access to DIContainer we will need to call GetReference for every needed dependency. By using reflection we may shorten code responsible for fetching objects. We just need an simple attribute to mark which fields should be fulfilled by DIConatiner.

public class DIInject : System.Attribute { }

public class DIContainer : MonoBehaviour
{
    ...

    public void Fetch(object objToFill, bool forceFetch = false)
    {
        FieldInfo[] fields = objToFill.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
        foreach (FieldInfo field in fields)
        {
            if (field.GetCustomAttribute<DIInject>(true) != null)
            {
                var currentValue = field.GetValue(objToFill);
                if (currentValue == null || forceFetch)
                {
                    var fieldValue = FindDependency(field.FieldType);
                    field.SetValue(objToFill, fieldValue);
                }
            }
        }
    }

    ...
}

How will look like classes which are using DIConatiner? Below is an example. CharacterController and CameraController classes register themself in conatiner on Awake. Then CameraConfigurationView gets references by Fetch method called in Start. It’s important that objects have to be registered in container before we try to get their references.

public class CharacterController : MonoBehaviour
{
    private void Awake()
    {
        DIContainer.Instance.Register(this);
    }
}

public class CameraController : MonoBehaviour
{
    private void Awake()
    {
        DIContainer.Instance.Register(this);
    }
}

public class CameraConfigurationView : MonoBehaviour
{
    [DIInject]
    private CameraController cameraController;
    [DIInject]
    private CharacterController characterController;

    private void Awake()
    {
        DIContainer.Instance.Register(this);
    }

    private void Start()
    {
        DIContainer.Instance.Fetch(this);
    }
}

Summary

Presented DIConatiner is a very simple implementation just to showcase a dependency injection concept and has some limitations. For example we can only register one object of specific type. If we wanted to have more objects of the same type, the easiest way is to create new class which will keep a collection e.g. List. Then instead of registering single object in the DIContainer, we register an object of our new class (collection holder) and add elements to the collection.

More robust implementation of DIContainer is available on my bitbucket and can be added to the project via Package Manager: https://bitbucket.org/pdgames_net/dicontainer/src. It provides an abstract class to extend, what allows to have multiple DIContainers in the project. In addition, it gives possibility to register objects in specific contexts. Contexts can be used to solve problem with registering multiple objects of the same class in the container. For example if we have two characters which use the same classes we can register them in two separate contexts.

There are also more complex solutions for Unity like Zenject: https://github.com/modesttree/Zenject. It’s a framework which allow to create injection rules to define how dependencies are distributed in our project. It’s very popular tool and many big game development companies are using it. For this reason it’s worth to know it 🙂

Materials

Leave a Reply

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