Unity3d e dependency injection
Dependency Injection
É um padrão que prega o tratamento de dependências através de um arquivo externo, ou seja qualquer dependência que uma classe tem é gerenciada por um arquivo, o que faz com que as classes nunca ‘saibam’ como acessar uma dependência.
As consequências desse desacoplamento são:
- Maior índice de reaproveitamento de código entre projetos
- Facilidade na criação de Moocs para testes unitários
- Facilidade em seguir o princípio de responsabilidade única (SOLID)
Zenject
Zenject é um framework de injeção de dependência que suporta a unity engine e suas peculiaridades, inspirado no framework Ninject para ASP.NET possui as seguintes features:
- Injection into normal C# classes or MonoBehaviours
- Constructor injection (can tag constructor if there are multiple)
- Field injection
- Property injection
- Injection via [PostInject] method parameters
- Conditional binding (eg. by type, by name, etc.)
- Optional dependencies
- Support for building dynamic object graphs at runtime using factories
- Injection across different Unity scenes
- Support for global, project-wide bindings to add dependencies for all scenes
- “Scene Decorators” which allow adding functionality to a different scene without changing it directly
- Ability to validate object graphs at editor time including dynamic object graphs created via factories
- Nested Containers aka Sub-Containers
- Support for Commands and Signals
- Ability to easily define discrete ‘islands’ of dependencies using ‘Facade’ classes
- Ability to automatically add bindings by dropping ZenjectAutoBinding on a game object in your scene
- Auto-Mocking using the Moq library
Utilização
Um exemplo simples de como se utiliza a injeção de dependência:
//Sem injeção de dependências
public class Foo
{
ISomeService _service;
public Foo()
{
_service = new SomeService();
}
public void DoSomething()
{
_service.PerformTask();
...
}
}
// Com injeção de dependências
public class Foo
{
ISomeService _service;
public Foo(ISomeService service)
{
_service = service;
}
public void DoSomething()
{
_service.PerformTask();
...
}
}
O que se pode perceber do código acima é que se utiliza Interfaces para que o Zenject injete as classes com as implementações necessárias, onde em determinado momento pode-se injetar uma implementação de teste para a realização das baterias de teste e em ambiente de produção utilizar a implementação real.
Registrando as dependências
Um simples HelloWorld com o registro das dependências pode ser analisado abaixo:
using Zenject;
using UnityEngine;
using System.Collections;
public class TestInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.Bind<Foo>().ToSingle();
Container.Bind<IBar>().ToSingle<Bar>();
}
}
public class Foo
{
IBar _bar;
public Foo(IBar bar)
{
_bar = bar;
}
}
A classe Foo receberá uma única instância de Bar e qualquer classe que precisar da dependência de Bar receberá a mesma dependência, o que pode mudar através de outros binds caso precise criar uma nova instância sempre que alguém precisar.
Binds
- ToSingle - Injeta como singleton
Container.Bind<IFoo>().ToSingle<Foo>();
Container.Bind<IBar>().ToSingle<Foo>();
- ToInstance - Injeta uma instância específica
Container.Bind<Foo>().ToInstance(new Foo());
Container.Bind<string>().ToInstance("foo");
Container.Bind<int>().ToInstance(42);
- ToTransient - Injeta uma instância nova do objeto
Container.Bind<IFoo>().ToTransient<Foo>();
- ToSinglePrefab - Injeta um prefab da unity
Container.Bind<FooMonoBehaviour>().ToSinglePrefab(PrefabGameObject);
Existem muitos binds que trabalham com prefabs, classes, metodos e propriedades, que podem ser verificados na documentação
Um CheatSheet muito útil com is binds do Zenject CheatSheet