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

Referências

Zenject docs