Skip to content

DI Automática

A toolbox de injeção automática de dependências do MiddleHaven é chamado Wiring Toolbox.

O conceito de injeção automática de dependências já é bastante conhecido graças a frameworks como o PicoContainer e o Spring. Recentemente este conceito foi incluído na especificação EJB 3.0. O conceito, é simples; um objecto de uma certa classe normalmente precisa de objetos de outras classes para funcionar. Isso cria uma dependência entre eles. Normalmente esta dependência é satisfeita passando uma instancia do objecto, manualmente, no construtor ou num método modificador (um método set). Este ato de atribuir as dependências ao objeto é conhecido como injeção ou wiring (“unir com fiação”). O nome “injeção” estabelece que ha um objeto injetado, um injetando e um objecto injetor. O nome “wiring” estabelece a ideia de dois objetos que são associados. Na prática ambos dão o mesmo resultado e trata-se apenas de uma diferença na nomenclatura.

A ideia da injeção automática é que seja o sistema a descobrir e instanciar o objeto certo em runtime e não o programador em tempo de codificação. Isto porque, a injeção manual leva ao uso de padrões de projeto como Factory e ServiceLocator que usados em demasia tornam o código dificil de manter já que a injeção não está centralizada e pior que isso, é controlada pelo próprio objeto que precisa das dependências. Ele assume responsabilidade de saber e encontrar as suas dependências e isso viola o Principio de Inversão de Controle (IoC).

Para que o sistema sera OO e respeite o Principio de Inversão de Controle precisamos de um outro objeto que controle a injeção das dependências : o injetor.

O MiddleHeaven usa os termos “wiring” e “wire” em vez de injeção. Para o MiddleHeaven o Injetor é uma peça fundamental mas que funciona por detrás dos panos. Não existe uma classe “injetor” e o programador final não mexe diretamente com ele.

Arquitetura

Um sistema de Injeção Automática de Dependência é composto por várias partes. O controlador do processo normalmente chamado de Injetor é a parte que sabe como procurar as dependências e injetá-las. No MiddleHeaven o injetor está escondido e o papel de fornecer acesso às suas funcionalidades cabe ao WiringService. O WiringService é o serviço central da Wiring Toolbox.

Se os objetos já existem em outros contextos ( por exemplo, objetos que existem num contexto JNDI) precisamos de objetos saibam onde e como obter os objetos. Quando o objeto necessário não é encontrado ele tem que ser construído. Para isso precisamos de objetos que saibam criar os objetos. No MiddleHeaven os objetos Resolver podem ter ambos os papeis de criador e localizador. Para que o MiddleHeaven possa encontrar os objetos os Resolvers têm que ser registrados junto ao WiringService.

Quando um objeto é criado, as suas dependências têm que ser resolvidas, primeiro as do construtor e depois aquelas associadas via um método modificador. Contudo, objetos da mesma classe A, que dependem de objetos da classe B, não necessariamente dependem da mesma instancia da classe B. O exemplo clássico é um corretor ortográfico. O corretor precisa de um dicionário, mas nem todos os corretores usam o mesmo dicionário, uns podem usar português, enquanto outros usam o dicionário inglês. Para conseguir distinguir os vários casos precisamos ter controle sobre o vinculo (Binding) entre o objeto e dependência em que estamos interessados.

O vinculo pode ser definido por outro objeto ou pelo próprio objeto. Ao ser definido pelo próprio objeto não é possível que ele especifique exatamente qual outro objeto ele quer, já que isso seria uma violação do Principio de Inversão de Controle que estamos tentado respeitar ao máximo. Então , a informação contida no objeto é lida pelo injetor como uma dica de qual objeto deve ser injetado naquele ponto. Os vários vínculos (Binding) também devem ser registrados junto ao WiringService.

ObjectPool

O WiringService tem vários tipos de configuração e permite ter acesso ao ObjectPool. O ObjectPool (piscina de objetos) representa o local onde os objetos estão, prontos a serem obtidos e injetados em outros objetos. O objeto é obtido pelos métodos getInstance() de ObjectPool. De certa forma o ObjectPool funciona como um objecto Factory/ServiceLocator, inteligente que consegue determinar as dependências sozinho e carregar os objetos sozinho (apenas com configuração e não com programação).

Escopos

Uma vez criado e devidamente injetado o objeto é devolvido a quem o requisitou. Pode ser interessante guardar esse objeto para uso futuro evitando ter que passar de novo pelo processo de procura e criação seguindo o padrão Shared Object ( apenas uma instancia do objeto existe, e todo o mundo usa a mesma). Ou pode ser interessante recuperar o objeto de um outro lugar, por exemplo, de um contexto JNDI. Para permitir a implementação destas regras o ObjectPool é dividido em regiões chamadas escopos (Scope). Todos os objetos existem realmente dentro de um escopo e não no ObjectPool diretamente.

Os escopos permitem ter um grau de controle de como os objetos são resolvidos antes que o Resolver seja invocado. Cada escopo conta com um ScopePool (piscina de escopo) onde os objetos são mantidos. Sendo o ScopePool um objeto, ele também passa pelo ciclo de injeção, sendo possível injetar um ScopePool com outros objetos. O MiddleHeaven conta com alguns escopos out-of-the-box:

  • Default – este é o escopo principal. Este escopo não guarda os objetos de forma alguma. Cada vez que um objeto é requisitado deste escopo, ele é criado do zero. Este é o comportamento padrão.
  • Shared – este escopo guarda os objetos de forma que se um objeto é requisitado várias vezes, sempre a mesma instancia é retornada. Em outros frameworks isto é chamado erramente de singleton. Os objetos no escopo shared não têm que implementar o padrão singleton (embora possam). O escopo Shared não torna os objetos “singleton” os torna Shared Object (Objeto Partilhado).
  • Service – este é um escopo especial relacionado à Service Toolbox. Os objetos guardados neste escopo são realmente serviços ( no sentido definido pela Service Toolbox). Os serviços são parecidos com objetos shared, mas têm um ciclo de vida que permite que uma vez injetados em outro objetos, possam ser modificados em runtime. Isto é útil para o redeploy de serviços em que o serviço está indisponível durante um breve período de tempo, enquanto a implementação real é trocada. Este é um escopo muito usado, então o MiddleHeaven dá um suporte especial para ele. O WiringService está registrado neste escopo, o que permite injetar o WiringService em qualquer outro objeto. Isto permite utilizá-lo no sem modo semelhante a uma fábrida de objetos.
  • Property – este escopo está associado, normalmente, a um arquivo de internacionalização, mas pode ser um simples arquivo de propriedades para configuração ( endereço do banco de dados, por exemplo). É principalmente uma forma de injetar Strings.

Escopo Local

Em algumas situações a injeção tem que ser feita baseada em um contexto local. O exemplo clássico é a injeção de um controller web com informações presentes no request ou no session. O MiddleHeaven suporta este tipo de injeção em objetos específicos (como os controlers).

Por enquanto o suporte a escopos locais é nativo ao MiddleHeaven ( i.e. não pode ser configurado ou alterado pelo programador final). O único escopo local suportado neste momento é o contexto web utilizado exatamente para a injeção de controlers.

Mais sobre esse assunto é discutido no toolbox web.

Pontos de Injeção

Para que o processo de injeção funcione o injetor precisa conhecer quais dependências satisfazer e como as satisfazer. Essa informação o MiddleHeaven chama ponto de injeção. Existem, basicamente, três categorias de pontos de injeção. No construtor, em métodos e em atributos. A injeção pelo construtor é a mais importante, já que, se o objeto não pode ser criado sem que que esse objetos sejam passados corretamente (passar null não vai adiantar).Após ser criado o objeto conta com métodos e atributos que também podem ser usados como pontos de injeção após a criação do objeto. Normalmente a injeção via construtor é usada para dependências obrigatórias ( o objeto não funciona sem elas) e a injeção por métodos/atributos é usada para dependências opcionais.

O MiddleHeaven não encoraja o uso de injeção por atributos que não sejam publico , embora o suporte, porque isso viola o Principio de Encapsulamento. Alguns toolboxes precisam fazer esse tipo de injeção devido ao seu dominio de atuação, mas isso é feito sob o controle desse toolbox e não a mando do programador final. Como programador final é aconselhável que use apenas a injeção por construtor e método modificador de forma a seguir boas práticas de programação orientada a objetos.

Binding

Como colocar objetos no ObjectPool para que sejam encontrados pelo WiringService durante a injeção ? Existem várias formas para fazer isso. Como foi dito, objetos que estejam associados a um escopo estão acessível ao ObjectPool. Então uma forma é colocar o objeto no escopo. Isso é independente de como os objetos chegaram a esse escopo. No exemplo do escopo de JNDI o objeto já existe no escopo ou é colocado por meio externos ao MiddleHeaven ( por exemplo, pelo próprio servidor de aplicação). Ao usar o escopo de serviços os objetos são disponibilizados porque foram registrados o ServiceRegistry do Service Toolbox. Para o escopo padrão o objeto nem sequer precisa existir. O único requisito é que exista um vinculo definido e a classe do objeto esteja no classpath.

A vinculação ao objeto pode dar-se de três formas:

  • Pela Classe – o ponto de injeção é vinculado a uma classe. Essa classe tem que ser compatível com o ponto de extensão. O MiddleHeaven irá criar um objeto dessa classe quando a injeção for requisitada. Se esse objeto depende de outros os vínculos são seguindo num ciclo recursivo até que o objeto requisitado possa ser devolvido ou se determine que alguma dependência está faltando.
  • Pela instancia – o ponto de injeção é vinculado a uma instancia de uma classe compatível com o ponto de injeção. Essa instancia será injetada sempre que aquele vinculo for invocado.
  • Por um Resolver – o ponto de injeção é vinculado a um objeto Resolver que irá obter o objeto a ser injetado.

Ativação

O MiddleHeaven utiliza o conceito de ativador. Um ativador é um objeto responsável por obter todas as dependências necessárias para a criação de um objeto e disponibilizar o objeto já construido. Ele atua como uma Factory. A diferença essencial do ativador é a resolução de dependências. Os ativadores são recolhidos as suas interdependências são analisadas e só depois os ativadores são invocados garantido desta forma que os objetos que precisam ser injetados foram criados anteriormente. Após esse processo inicial, novos ativadores podem ser detetados e invocados adicionando objetos ao ObjectPool em runtime. De forma semelhante os ativadores podem ser desabilitados retirando os objetos do ObjectPool.

Este mecanismo de ativação é essencial a diferentes funcionalidades do MiddleHeaven. Ele é a base para a funcionalidade de instalação/desinstalação de modulos de aplicação e serviços “a quente”. O processo de ativação é fundido ao processo de bootstrap permitindo que o Container forneça seus próprios ativadores. Por exemplo, quando o MiddleHeaven rodar dentro do Tomcat um serviço de JNDI pode estar opcionalmente disponível, enquanto que se rodar num JBoss o serviço de JNDI estará obrigatoriamente disponivel assim como o de envio de email.

Uso

A Wiring Toolbox é mais dificil de explicar do que de exemplificar, mas os exemplos são extensos.

Sendo que ha suporte a qualquer modelo de definição de pontos de injeção existem muitas mais opções reais que as demonstradas ( usar anotações do Java ou de outro framework que não do do MiddleHeaven). Aqui ficam exemplos usando apenas os recursos nativos ao MiddleHeaven.

Para definir um ponto de injeção basta anotá-lo com @Wire. Para construtores a anotação é dispensável se apenas existir um construtor definido. Para métodos e atributos a anotação é obrigatória para definir o ponto de injeção. A injeção sempre é obrigatória por padrão. A anotação @Wire conta com um parametro required que alterado para false torna a injeção opcional (Se o objeto não for encontrado o processo continua assim mesmo). Conta inda como o parametro shareable que quando colocado false obriga o injetor a criar uma instancia diferente (não partilhada) para ser injetada nesse ponto. O MiddleHeaven já cria uma nova instancia diferente por padrão, mas apenas no escolo padrão. Se o objeto exige ser injetado com uma instancia diferente e isso não for possivel, devido ao escopo requisitado ou qualquer outro impedimento uma exceção será lançada.

A anotação @Wire pode ser utilizada ainda nos parâmetros, para alterar a obrigatoriedade ou a partilha

Para criar o ponto de injeção é só isso que precisa. Agora, para configurar o ponto de injeção. Se nada mais for dito o injeto irá procurar um objeto, no escopo padrão, da classe do parâmetro/atributo e a primeira encontrada será retornada. Para escolher o escopo basta anotar com anotação atrelada ao escopo (isso também é configurado como um vinculo). Por exemplo, para dizer que o objeto tem que ser obtido do escopo de serviço é só anotar também com @Service. Para o escopo de objetos partilhados é @Shared. A associação entre a anotação e o ScopePool também é determinada por um vinculo e portanto, pode ser configurada.

Acontece que é possível precisar especificar parâmetros do serviço – por exemplo, escolher o dicionário em inglês – para isso basta usar @Params que recebe um array de strings to tipo “chave=valor” – por exemplo, @Params(“lang=en”). Imagine agora que está tentando injetar uma String. Este é um dos objetos mais usados em Java, portanto qual delas usar ? O injetor irá procurar no escopo de propriedades e poderia usar @Params(“name=chaveDaMensagem”) para escolher a chave. Para simplificar, use @Name(chaveDaMensagem”). O @Name pode ser ainda usado para o escopo de nomes e diretórios, no escopo web ou para distinguir dois objetos que mapeiam a mesma classe. Repare que não existem ids como no Spring. O @Name apenas precisa ser usado em caso de ambiguidade. No caso padrão não haverá problema.

01
02 public void simpleTest (){
03 final MockDisplay md = new MockDisplay () ;
04
05 // obter diretamente do ServiceRegistry para comodidade de teste
06 ServiceRegistry.getService ( WiringService. class ) .getObjectPool ()
07 .addConfiguration ( new BindConfiguration (){ // configura vinculos
08
09 @Override
10 public void configure ( Binder binder ) {
11 binder.bind ( Displayer. class ) .toInstance ( md ) ;
12 binder.bind ( Message. class ) .to ( HelloMessage. class ) ;
13 binder.bindProperty ( String. class ) .named ( “hello.message” ) .toInstance ( “Hello” ) ;
14 binder.bindProperty ( String. class ) .named ( “hello.name” ) .toInstance ( “World” ) ;
15 }
16
17 })
18 .getInstance ( Greeter. class ) // obtem objeto
19 .sayHello () ; // invoca método no objeto
20
21 assertEquals ( “Hello, World” , md.getSaing ()) ;
22 }
23

Código 1:

Neste exemplo podemos ver como vincular classes a instancias e classes de implementação. também podemos ver como vincular instancias a classes usando o argumento named.

01
02
03 public void testWiringServiceWithParams (){
04
05
06 ObjectPool pool = this .getWriringContext ()
07 .addConfiguration ( new BindConfiguration (){
08
09 @Override
10 public void configure ( Binder binder ) {
11 binder.bind ( DictionaryService. class ) .in ( Service. class ) ;
12 binder.bindScope ( Service.class, ServiceScope. class ) ;
13 }
14
15 }) ;
16
17
18 ServiceContext serviceContext = pool.getInstance ( ServiceContext. class ) ;
19
20 ParamsMap paramsEn = new ParamsMap () ;
21 paramsEn.put ( “lang” , “en” ) ;
22 serviceContext.register ( DictionaryService.class, new HashDictionaryService ( “en” ) ,paramsEn ) ;
23
24 ParamsMap paramsPT = new ParamsMap () ;
25 paramsPT.put ( “lang” , “pt” ) ;
26 serviceContext.register ( DictionaryService.class, new HashDictionaryService ( “pt” ) ,paramsPT ) ;
27
28
29 DictionaryService enDic = pool.getInstance ( DictionaryService.class, paramsEn ) ;
30
31 assertEquals ( “en” , enDic.getLang ()) ;
32
33 DictionaryService ptDic = pool.getInstance ( DictionaryService.class, paramsPT ) ;
34
35 assertEquals ( “pt” , ptDic.getLang ()) ;
36
37 // with no params the service scope will choose one of the existing implementations
38 DictionaryService eDic = pool.getInstance ( DictionaryService. class ) ;
39
40 assertNotNull ( eDic ) ;
41
42 }
43

Código 2:

Neste exemplo podemos ver como vincular escopos e como usar parametros para diferenciar entre implementações.

Por detrás dos panos

Este toolbox é completamente implementado com recursos da plataforma Java sem utilizar nenhuma API externa. Isto é proposital. Primeiro por causa da diretiva principal de isolar tecnologias. Segundo porque as API que existem não são suficientemente genéricas. Afinal anotações amarram o seu código aos frameworks tanto quanto implementar interfaces ou estender classes. É uma questão de quanto amarram, e não se amarram ou não. Isto é um grande problema. Imagine que você já tem um conjunto de classes que quer injetar e ver injetadas no seu novo sistema construído como MiddleHeaven. O MiddleHeaven não pode lhe exigir que coloque anotações em todas elas. Isso implica em alterar o código original. Por isto o MiddleHeaven tem que permitir estender-se até às suas necessidades.

Deixe um Comentário

Deixe uma Resposta

Preencha os seus detalhes abaixo ou clique num ícone para iniciar sessão:

Logótipo da WordPress.com

Está a comentar usando a sua conta WordPress.com Terminar Sessão / Alterar )

Imagem do Twitter

Está a comentar usando a sua conta Twitter Terminar Sessão / Alterar )

Facebook photo

Está a comentar usando a sua conta Facebook Terminar Sessão / Alterar )

Google+ photo

Está a comentar usando a sua conta Google+ Terminar Sessão / Alterar )

Connecting to %s

%d bloggers like this: