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.