Dependency Injection in Unity3d

    Good afternoon, dear colleagues!

    It so happened that by the time I started working with Unity3D, I had four years of .NET development experience. For three years out of these four, I have successfully applied dependency injection in several large industrial projects. This experience turned out to be so positive for me that I tried to bring it to game dev.
    Now I can already say that I started this for good reason. After reading to the end, you will see an example of how dependency injection allows you to make the code more readable and simpler, but at the same time more flexible, and at the same time more suitable for unit testing. Even if you first hear the phrase dependency injection - it's okay. Do not pass by! This article is intended as a fact-finding article, without diving into subtle matters.

    A lot has been written about dependency injection, including on Habré. There are a large number of solutions for DI - the so-called DI-containers. Unfortunately, upon closer inspection, it turned out that most of them are heavy and overloaded with functionality, so I was afraid to use them in mobile games. For some time I used the Lightweight-Ioc-Container (all links are given at the end of the article), but later I refused it and, I repent, wrote my own. In my defense, I can only say that I tried to create the most simple container, sharpened for use with Unity3D and easy extensibility.

    Example

    So, we will consider the application of dependency injection in a specially simplified example. Suppose we are writing a game in which a player must fly forward on a spaceship while dodging meteorites. All he can do is push the buttons to move the ship left and right to avoid collisions. It should turn out something like a runner, only on a space theme.
    We already have the KeyboardController class, which will inform us about the buttons pressed, and the SpaceShip class, which can move beautifully left and right, while throwing out particle flows. Tie it all together:

    public class MyClass
    {
        private SpaceShip spaceShip;
        private KeyboardController controller;
        public void Init()
        {
            controller = new KeyboardController();
            GameObject gameObject = GameObject.Find("/root/Ships/MySpaceShip");
            spaceShip = gameObject.GetComponent();
        }
        public void Update()
        {
            if (controller.LeftKeyPressed())
                spaceShip.MoveLeft();
            if (controller.RightKeyPressed())
                spaceShip.MoveRight();
        }
    }
    


    The code is excellent - simple and straightforward. Our game is almost ready.

    Ahhh !!! The chief designer has just arrived and said that the concept has changed. Now we are writing not for PC, but for tablets and the ship you need to control not the buttons, but the tilt of the tablet left-right. And in some scenes, instead of flying in a spaceship, we will have a funny alien running along the corridor. And this alien must be controlled by swipes. It's all redo !!!
    Or not all?
    Even if we introduce interfaces to reduce connectivity, it will not give us anything:
    public class MyClass
    {
        private IControlledCharacter spaceShip;
        private IController controller;
        public void Init()
        {
            controller = new KeyboardController();
            GameObject gameObject = GameObject.Find("/root/Ships/MySpaceShip");
            spaceShip = gameObject.GetComponent();
        }
        public void Update()
        {
            if (controller.LeftCmdReceived())
                spaceShip.MoveLeft();
            if (controller.RightCmdReceived())
                spaceShip.MoveRight();
        }
    }
    


    I even renamed the LeftKeyPressed () and RightKeyPressed () methods to LeftCmdReceived () and RightCmdReceived (), but that didn’t help (there should be a sad smiley) in the code, the class names of KeyboardController and SpaceShip remain. It is necessary somehow to avoid binding to specific implementations of interfaces. It would be cool if interfaces were passed to our code right away. For example, like this:
    public class MyClass
    {
        public IControlledCharacter SpaceShip { get; set; }
        public IController Controller { get; set; }
        public void Update()
        {
            if (Controller.LeftCmdReceived())
                SpaceShip.MoveLeft();
            if (Controller.RightCmdReceived())
                SpaceShip.MoveRight();
        }
    }
    


    Hmm, look! Our class has become shorter and more readable! The lines related to finding an object in the scene tree and getting its component disappeared. But on the other hand, these lines should be present somewhere? You can’t just pick them up and throw them away? So, we simplified our class to complicate the code that uses it?

    Not certainly in that way. The objects we need can be transferred automatically to the properties of our class - “inject”. This can be done for us by a DI container!

    But for this we will have to help him a little:
    1. Clearly outline the dependencies of our class. In the above example, we do this using properties with [Dependency] attributes:
    public class MyClass
    {
        [Dependency]
        public IControlledCharacter SpaceShip { private get; set; }
        [Dependency]
        public IController Controller { private get; set; }
        public void Update()
        {
            if (Controller.LeftCmdReceived())
                SpaceShip.MoveLeft();
            if (Controller.RightCmdReceived())
                SpaceShip.MoveRight();
        }
    }
    


    2. We must create a container and tell it where to get the objects for these dependencies from - configure it:
    var container = new Container();
    container.RegisterType();
    container.RegisterType();
    container.RegisterSceneObject("/root/Ships/MySpaceShip");
    

    Now make the container collect the object we need:
    MyClass obj = container.Resolve();
    

    In obj all necessary dependencies will be affixed.

    How it works?

    What happens when we ask a container to provide an object of type MyClass?
    The container searches for the requested type among the registered ones. In our case, the MyClass class is registered in the container using RegisterType (), which means - upon request, the container must create a new object of this type.
    After creating a new MyClass object, the container checks to see if it has dependencies? If there are no dependencies, the container will return the created object. But in our example of dependencies there are two integers and the container tries to resolve them in the same way as the user calls Resolve <> ().

    One of the dependencies is an IController type dependency. RegisterType() tells the container that when requesting an IController object, you need to create a new object of type KeyboardController (and of course allow its dependencies, if any).
    Where to get the object for the second IControlledCharacter dependency, we told the container using RegisterSceneObject ("/ root / Ships / MySpaceShips"). There is no need to create anything from the container. It is enough to find the game object along the path in the scene tree, and for it, select the component that implements the specified interface.

    What else can our DI container do? A lot of things. For example, it also supports singletones. In the example above, anyone who requests an IController object will receive a copy of their KeyboardController. We could register the KeyboardController as a singleton, and then all applicants would receive a link to the same object. We could even create the object ourselves, with the help of 'new', and then transfer it to the container so that it will distribute the object to the afflicted. This is useful when a singleton requires some kind of nontrivial initialization.

    Then the dear reader narrowed his eyes suspiciously and asked - but is this not over-engineering? Why fence such gardens when there is a good old singleton recipe with "public static T Instance {get;}"? I answer - for two reasons:
    1. The call to the static singleton is hidden in the code, and at first glance it can be impossible to say whether our class accesses the singleton or not. In the case of using dependency injection through properties, everything is clear as God's day. All dependencies are visible in the class interface and are marked with Dependency attributes. Here, in addition to this, coding convention requires that all class dependencies be grouped together and go immediately after private variables, but before the constructors.
    2. Writing unit tests for a class that accesses a traditional singleton is generally not a trivial task. When using a DI container, our life is greatly simplified. It is only necessary to make the class access the singleton through the interface, and register the corresponding mock in the container. In general, this applies not only to singletones. Here is an example unit test for our class:

    var controller = new Mock();
    controller.Setup(c => c.LeftCmdReceived()).Returns(true);
    var spaceShip = new Mock();
    var container = new Container();
    container.RegisterType();
    container.RegisterInstance(controller.Object);
    container.RegisterInstance(spaceShip.Object);
    var myClass = container.Resolve();
    myClass.Update();
    spaceShip.Verify(s => s.MoveLeft(), Times.Once());
    spaceShip.Verify(s => s.MoveRight(), Times.Never());
    

    I used Moq to write this test. Here we create two moka - one for IController and one for IControlledCharacter. For IController we set the behavior - the LeftCmdReceived () method should return true when called. We register both moka in the container. Then we get the MyClass object from it (both of which will now be our moks) and call Update () on it. Then we check that the MoveLeft () method was called once, and MoveRight () - not a single one.
    Yes, of course, moki could be stuck in MyClass "pens", without any container. However, let me remind you, the example is specially simplified. Imagine that you need to test not one class, but a set of objects that should work in conjunction. In this case, we will replace only individual entities in the container with mokas that are by no means suitable for testing - for example, classes that climb into the database or network.

    Dry residue

    1. Turning to the container, we get an already assembled object with all its dependencies. As well as the dependencies of its dependencies, the dependencies of the dependencies of its dependencies, etc.
    2. Class dependencies are very clearly identified in the code, which greatly improves readability. One glance is enough to understand which entities the class interacts with. Readability, in my opinion, is a very important quality of the code, if not the most important. Easy to read -> easy to modify -> less likely to introduce bugs -> code lives longer -> development moves faster and costs less
    3. The code itself is simplified. Even in our trivial example, we managed to get rid of the object search in the scene tree. And how many such similar pieces of code are scattered in real projects? The class has become more focused on its core functionality.
    4. Additional flexibility appears - changing container settings is easy. All the changes that are responsible for linking your classes to each other are localized in one place
    5. From this flexibility (and the use of interfaces to reduce connectivity) comes the ease of unit testing of your classes
    6. And last. Bonus for those who patiently read to this place. We unexpectedly received an additional metric of code quality. If your class has more than N dependencies, then something is wrong with it. Perhaps it is overloaded and it is worth sharing its functionality between several classes. N substitute yourself

    Of course, you guessed that the search for dependencies in the scene tree is what I started writing my own DI container for. The container is very simple. You can get its sources and a demo project here: dl.dropboxusercontent.com/u/1025553/UnityDI.rar
    Please read and criticize.

    The article also mentioned:
    Lightweight-Ioc-Container
    Moq

    PS. Posted code on Github: github.com/yaroslav-gurilev/UnityDI

    Also popular now: