Ninject – O Ninja das dependências

O que é o Ninject?

O Ninject é uma biblioteca de software aberto que providencia uma framework de injecção de dependências (Dependency Injection ou DI) leve, fácil de integrar e de utilizar.

O padrão Dependency Injection determina que as dependências entre módulos da aplicação ou classes são determinadas por configuração ao invés de inicializadas pelo programador em código, pelo que permite aumentar o grau de desacoplamento das aplicações, garantindo maior flexibilidade na inicialização e execução da aplicação. Este padrão surge muitas vezes associado ao padrão Inversion of Control (IoC) que determina que o controlo aplicacional não é controlado na integrada pelo programador, mas sim por uma framework ou runtime.

O padrão DI é bastante utilizado em software empresarial porque, entre outras razões, ajuda a manter o desacoplamento dos módulos aplicacionais, permite manter o controlo do ciclo de vida dos objectos e facilita a implementação de testes automáticos.

O Ninject implementa o padrão DI, providenciando a injecção de dependências de classes ou módulos nas aplicações onde é usado, permitindo ao programador configurar as dependências com que a aplicação é inicializada e alterá-las em runtime, se necessário.

O que devo saber antes de utilizar o Ninject?

O Ninject pode ser utilizado em todos os tipos de aplicações desenvolvidas em .NET. No caso das Windows Applications ou Console Applications basta incluir a biblioteca no projeto e começar a utilizar.

Já se pretender integrar o Ninject numa aplicação Web MVC ou numa biblioteca de serviços WCF, será necessário utilizar alguns plugins que permitam tratar da “canalização” necessária para garantir que as factories das frameworks MVC e WCF utilizam o Ninject como “fornecedor” de instâncias.

Vejamos os princípios: Suponhamos que temos uma classe UserManager que faz uso de uma classe UserReader.

50_ninject_1

Num cenário clássico, o código que representa esta situação seria parecido com:

class UserReader
{
    // User reader methods
}

class UserManager
{
    private UserReader userReader;
    public UserManager()
{
        // A classe UserManager está a criar uma instância da classe UserReader
        this.userReader = new UserReader();
}
}

Esta implementação num projecto de larga escala levanta duas questões essenciais:

  1. Se o construtor da classe UserReader for alterado, todas as classes que utilizam a classe UserReader terão que ser também alteradas. Tal pode levar a que os próprios construtores dessas classes seja também alterados, o que vai gerar uma onda de alterações em várias classes do projecto;
  2. A proliferação de chamadas a construtores poderá escalar de tal forma que se perde o controlo de quantas instâncias da classes estão de facto a ser criadas e utilizadas pela aplicação.

Ao utilizar uma framework de injecção de dependências, a gestão das instâncias das classes é efectuada pelo container que “injecta” a instância de uma classe sempre que está é necessária.

Neste cenário, teríamos uma implementação parecida com:

class UserReader
{
    // User reader methods
}

class UserManager
{
    private UserReader userReader;
    public UserManager(UserReader userReader)
    {
        // A classe UserManager recebe uma instância da classe UserReader
        this.userReader = userReader;
    }
}

Neste caso, o container saberia construir uma instância da classe UserReader e injectá-la-ia no construtor da classe UserManager, tornando esta classe completamente agnóstica à forma como a classe UserReader foi criada.

NOTA: Seria também possível utilizar uma factory class para este efeito, o que consistiria uma alternativa à utilização de uma framework de dependency injection (no entanto menos poderosa).

Como começo?

O ponto central do Ninject é a interface IKernel cuja implementação por omissão é a classe StandardKernel. Esta classe implementa um container que armazena a configuração dos mapeamentos de interfaces em classes bem como as instâncias de alguns objectos disponibilizados pelo container.

Toda a interacção com o container é efectuada por código usando uma API fluente e bastante completa. Para quem prefere usar XML, o plugin Ninject.Extensions.Xml permite efectuar a configuração do Ninject usando XML.

Para começar a utilizar o Ninject, é necessário importar o namespace Ninject e declarar uma variável do tipo StandardKernel.

using Ninject;

class Program
{
    static void Main(string[] args)
    {
        IKernel kernel = new StandardKernel();
    }    
}

O objecto kernel será o container da aplicação e deverá ser declarado no ponto de entrada da aplicação, tão cedo quanto possível.

Seguindo o exemplo anterior, podemos agora solicitar uma instância de uma variável do tipo UserManager ao Ninject sem necessitar de qualquer outra configuração adicional. Para tal, utiliza-se o extension method Get<> para obter a instância:

using Ninject;

class Program
{
    static void Main(string[] args)
    {
        IKernel kernel = new StandardKernel();
        UserManager manager = kernel.Get<UserManager>();
    }    
}

class UserReader
{
    // User reader methods
}

class UserManager
{
    private UserReader userReader;
    public UserManager(UserReader userReader)
    {
        this.userReader = userReader;
    }
}

Após a instrução Get<>, a variável manager do método Main fica populada com uma instância de UserManager que foi construída usando o construtor que recebe um UserReader. O Ninject percebeu que podia construir uma instância da classe UserReader para injectar no construtor da classe UserManager.

Neste caso, o Ninject aplicou o mecanismo de resolução automática de dependências que constrói instâncias de acordo com as seguintes regras:

  1. Caso o construtor não tenha argumentos este é invocado para criar a instância;
  2. Caso o construtor tenha argumentos, o Ninject tenta criar uma instância de cada uma das classes de argumento do construtor (aplicando as regras de resolução automática);
  3. Caso haja mais que um construtor com argumentos ou não seja possível resolver um dos argumentos do construtor, será lançada uma excepção.

NOTA: Caso a classe possua mais que um construtor o Ninject tentará utilizar o construtor sem argumentos (construtor default) para construir a classe. Caso esta não tenha nenhum construtor default, será necessário indicar ao Ninject qual o construtor que este deve utilizar para construir a classe.

Mecanismos de injecção

O exemplo acima utiliza o mecanismo constructor injection que faz com que o container inspeccione o construtor da classe, tente resolver as dependências deste (i.e. os argumentos do construtor) e chame o construtor passando as dependências identificadas.

O Ninject suporta ainda outro tipo de injecção: a property injection. Neste caso, o Ninject encara uma propriedade da classe como uma dependência a ser injectada na classe e tenta resolvê-la e injectá-la como se um argumento de um construtor se tratasse.

Para declarar que uma propriedade deve ser injectada, será necessário decorar a propriedade com o atributo [Inject].

O mecanismo de constructor injection é tipicamente mais utilizado que o property injection por este garantir que todas as dependências estão no construtor e que o objecto é inicializado correctamente no construtor. Outro motivo a favor da utilização de constructor injection é o facto de evitar que as classes “conheçam” o Ninject, já que desta forma raramente será necessário as classes injectadas utilizarem o Ninject.

E quando a resolução automática não consegue resolver?

Há casos em que a resolução automática não consegue resolver a dependência de forma inequívoca. Nestes casos, o Ninject lançará uma excepção e caberá ao programador dar indicação sobre como resolver a dependência.

Tipicamente, existem três casos em que a resolução automática não consegue resolver:

  1. Quando a classe tem mais que um construtor em que nenhum deles é o construtor default;
  2. Quando o construtor tem um argumento do tipo Interface;
  3. Quando o construtor tem um argumento com um tipo básico.

Nestas situações, o container disponibiliza um conjunto de métodos de configuração para instruir o Ninject sobre como proceder. O método Bind<>() permite indicar ao Ninject como resolver uma determinada classe ou interface.

Na primeira situação, é possível instruir o Ninject sobre qual o construtor a usar usando o método Bind<>().ToConstructor(). A utilização do método WithParameter() permite especificar valores a atribuir a certos parâmetros do construtor.

class UserReader
{
    public UserReader(string connectionString) { ... }
    public UserReader(string connectionString, int max) { ... }
}
    // Após declarar o kernel
kernel.Bind<UserReader>()
.ToConstructor<UserReader>((ctx) => new UserReader(string.Empty))
.WithParameter(new Parameter("connectionString", "myConnectionString", true));

Neste caso, foi necessário indicar qual o construtor a usar, bem como o valor do parâmetro a utilizar no tipo básico do construtor que foi escolhido. O método ToConstructor() instrui o Ninject para utilizar aquele construtor. O método WithParameter() indica ao Ninject qual o valor a atribuir ao parâmetro connectionString.

No caso em que o construtor necessita de uma interface, utiliza-se o método Bind<>().To<>():

class FacebookUserReader : IUserReader
{
    // Get user information from Facebook
}

class UserManager
{
    public UserManager(IUserReader userReader) { ... }
}

// Instrução que indica ao NInject que a classe que implemementa a interface IuserReader é a classe FacebookUserReader
kernel.Bind<IUserReader>().To<FacebookUserReader>();

Top tip: Um caso de injecção é utilizar o container para injectar um logger que, tipicamente, é inicializado em função da classe onde o logger é utilizado. Neste caso, é possível utilizar o método Bind<>().ToMethod() para garantir a inicialização por classe:

kernel.Bind<ILog>().ToMethod(ctx => LogManager.GetLogger(ctx.Request.Service));

class UserManager
{
   public UserManager(IUserReader userReader, ILog logger) { ... }
}
class UserReader
{
public UserReader(string connectionString, ILog logger) { ... }
}

Neste caso, a variável ctx.Request.Service contém o nome da classe para a qual foi solicitada a injecção. Quando uma classe requisitar uma instância de ILog, o Ninject irá executar a Func<> indicada no argumento do método ToMethod() para obter uma instância específica para a classe que a solicitou. No exemplo anterior, seriam injectadas duas instâncias diferentes de Logger, uma para cada classe e inicializadas com o nome da respectiva classe onde foi injectada.

Âmbito das dependências

Por omissão, o Ninject cria uma nova instância da classe sempre que há uma solicitação de injecção. Ou seja, sempre que uma classe é necessária o construtor é sempre invocado para satisfazer a dependência.

Ao declarar o binding com Bind<>().To<>().InSingletonScope() o Ninject garante que aquela classe será um singleton, ou seja é construída uma única vez e essa instância é utilizada em todas as dependências.

O binding Bind<>().To<>().InThreadScope() garante que é criada um instância por cada Thread da aplicação.

Utilizando o binding Bind<>().To<>().When() é também possível instruir o Ninject para injectar diferentes tipos em função de determinada condição ditada pelo método When().

Outras funcionalidades

O Ninject disponibiliza também mecanismos de carregamento de módulos e plugins que permitem conferir bastante dinamismo às aplicações.

Existem também extensões que oferecem funcionalidades de interceção de métodos que são particularmente úteis para controlo de segurança, auditing ou troubleshooting.

A extensão Ninject.Mvc oferece a integração plena do Ninject com o ASP.NET MVC, permitindo que os controllers sejam inicializados pelo container do Ninject.

Conclusão

O Ninject é um motor de injecção de dependências bastante completo e versátil que garante ao programador um excelente controlo sobre a forma como a aplicação é construída bem como o ciclo de vida dos objectos de suporte à aplicação.

A sua sintaxe fluída oferece um excelente mecanismo de configuração, evitando os longos ficheiros em formato XML típicos de outras frameworks de injecção de dependências.

Nos cenários em que a aplicação está sujeita a testes unitários ou de integração, uma framework de injecção de dependências é uma ferramenta importante para garantir a fácil utilização de mock objects. O Ninject possui um excelente suporte para alterar os bindings em runtime, facilitando em muito a execução dos testes.

Pela forma como está construído, apenas as classes de ponto de entrada das aplicações terão que “conhecer” o Ninject, sendo o resto da aplicação completamente agnóstica ao motor de injecção de dependências.

A utilização de módulos permite distribuir a inicialização dos vários componentes aplicacionais, garantido uma melhor organização do código e evitando ficheiros de configuração demasiado longos.