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
.
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:
- Se o construtor da classe
UserReader
for alterado, todas as classes que utilizam a classeUserReader
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; - 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:
- Caso o construtor não tenha argumentos este é invocado para criar a instância;
- 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);
- 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:
- Quando a classe tem mais que um construtor em que nenhum deles é o construtor default;
- Quando o construtor tem um argumento do tipo Interface;
- 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.