Good afternoon, dear colleagues!
It so happened that by the time I started working with Unity3D, I had four years of experience developing on .NET. Three years from 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 into game dev.
Now I can say that I started it for a 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 also more suitable for unit testing. Even if you first hear the phrase "dependency injection" - do not worry. Do not pass by! This article is intended as an introductory, without immersion in subtle matter.
About dependency injection is written a lot, including on Habré. There are also 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 a while I used 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 simplest container possible, sharpened for use with Unity3D and easy extensibility.
Example
So, consider the use of dependency injection on a specially simplified example. Suppose we are writing a game in which a player must fly forward in a spacecraft while dodging meteorites. All he can do is push the buttons to shift the ship left and right to avoid collisions. It should get something like a runner, only on a space theme.
We already have the KeyboardController class, which will tell us about the buttons pressed, and the SpaceShip class, which can move left and right beautifully, throwing out the particle flow. Let's 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<SpaceShip>(); } public void Update() { if (controller.LeftKeyPressed()) spaceShip.MoveLeft(); if (controller.RightKeyPressed()) spaceShip.MoveRight(); } }
The code turned out great - simple and straightforward. Our game is almost ready.
Ahhh !!! The chief designer had just arrived, and said that the concept had changed. We now write not under the PC, but under the tablets and the ship, you need to control not the buttons, but the tilts of the tablet left-right. And in some scenes, instead of flying in a spacecraft, we will have a funny alien running down the corridor. And this alien must be controlled by svaypami. This is all redo !!!
Or not all?
Even if we introduce interfaces to reduce connectivity, this will give us nothing:
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<SpaceShip>(); } 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 this did not help either (there should be a sad smiley) The code still contains the names of the KeyboardController and SpaceShip classes. It is necessary to somehow avoid binding to specific implementations of interfaces. It would be cool if interfaces were transferred 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 searching for an object in the scene tree and getting its component disappeared. But on the other hand, should these lines be present somewhere? Can't they be so easy to take and throw out? So, we simplified our class to complicate the code that uses it?
Not certainly in that way. The objects we need can be automatically transferred to the properties of our class - “inject”. DI-container can do this for us!
But for this we will have to help him a little:
1. Clearly identify the dependencies of our class. In the example above, 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 need to create a container and tell it where to get the objects for these dependencies — configure it:
var container = new Container(); container.RegisterType<MyClass>(); container.RegisterType<IController, KeyboardController>(); container.RegisterSceneObject<IControlledCharacter>("/root/Ships/MySpaceShip");
Now let the container collect the object we need:
MyClass obj = container.Resolve<MyClass>();
In obj all necessary dependences will be put down.
How it works?
What happens when we ask the container to provide an object of type MyClass?
The container is looking for the requested type among the registered. In our case, the class MyClass is registered in the container using RegisterType (), which means that the container must create a new object of this type upon request.
After creating a new MyClass object, the container checks if it has dependencies? If there are no dependencies, the container will return the created object. But in our example, the dependencies are as many as two and the container tries to resolve them in the same way as a user call Resolve <> ().
One of the dependencies is an IController type dependency. RegisterType <IController, KeyboardController> () 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 from it select the component that implements the specified interface.
What else can our DI-container? A lot of things. For example, he also supports singletons. In the example above, anyone who requests an IController object will receive their copy of the KeyboardController. We could register KeyboardController as a singleton, and then all who applied 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 distributes the object to the suffering. This is useful when a singleton requires some kind of non-trivial initialization.
Here, dear reader will squint suspiciously and ask - isn’t it overengineering? Why make a fuss when there is a good old singleton recipe with “public static T Instance {get;}”? I answer - for two reasons:
1. The reference to a static singleton is hidden in the code, and at first glance it is impossible to say whether our class refers to a singleton or not. In the case of using dependency injection through the properties, everything is clear as day. All dependencies are visible in the class interface and labeled with Dependency attributes. With us, in addition to this, the coding convention requires that all class dependencies be grouped together and go right after the private variables, but before the constructors.
2. Write unit tests for a class that refers to a traditional singleton. In general, the task is non-trivial. In the case of a DI container, our life is greatly simplified. You only need 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 singletons. Here is an example of a unit test for our class:
var controller = new Mock<IController>(); controller.Setup(c => c.LeftCmdReceived()).Returns(true); var spaceShip = new Mock<IControlledCharacter>(); var container = new Container(); container.RegisterType<MyClass>(); container.RegisterInstance<IController>(controller.Object); container.RegisterInstance<IControlledCharacter>(spaceShip.Object); var myClass = container.Resolve<MyClass>(); myClass.Update(); spaceShip.Verify(s => s.MoveLeft(), Times.Once()); spaceShip.Verify(s => s.MoveRight(), Times.Never());
To write this test, I used Moq. Here we create two mocks - one for the IController and the other for the IControlledCharacter. Set the behavior for IController - the LeftCmdReceived () method should return true when called. Both moka register in the container. Then we get the MyClass object from it (both of which dependencies are now our mocks) and call Update () on it. Then we check that the MoveLeft () method was called once, and the MoveRight () method was not called once.
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 substitute only individual entities in the container with mocks that are by no means suitable for testing - for example, classes that climb into the database or the network.
Dry residue
1. Turning to the container, we get the already assembled object with all its dependencies. As well as dependencies of its dependencies, dependencies of dependencies of its dependencies, etc.
2. Class dependencies are very clearly highlighted in the code, which greatly improves readability. One glance is enough to understand with what entities the class interacts. Readability, in my opinion, is a very important quality of the code, if not the most important one. 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 homogeneous pieces of code are scattered in real projects? The class has become more focused on its core functionality.
4. Additional flexibility appears - it is easy to change container settings. All changes 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) is the ease of unit testing of your classes.
6. And the last. Bonus for those who patiently read up to this place. We unexpectedly received an additional code quality metric. If your class has more 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 it that the search for dependencies in the scene tree is what I started writing my own DI container for. The container was very simple. You can
get its source and demo project here:
dl.dropboxusercontent.com/u/1025553/UnityDI.rarPlease read and criticize.
Also in the article mentioned:
Lightweight-Ioc-ContainerMoqPs. Laid out code on Github:
github.com/yaroslav-gurilev/UnityDI