Login Registre-se

Home > Artigos > Engenharia de Software >

Inversion Of Control - Containers de Inversão de Controle e o padrão Dependency Injection

Publicado por Tutoriais Admin em 17/08/2009 - 175.140 visualizações


comentários: 0

Martin Fowler

Na comunidade Java, tem havido uma onda de containers leves (lightweight containers) que ajudam a montar componentes de projetos diferentes em uma aplicação coesa. Por trás destes containers há um padrão comum de como eles fazem a amarração (wiring), conceito ao qual eles se referem sob o nome genérico de Inversão de Controle (Inversion of Control, ou IoC). Neste artigo, eu exploro o funcionamento deste padrão, sob o nome mais específico de Dependency Injection (Injeção de Dependências) e o contrasto com a alternativa Service Locator. A escolha entre eles é menos importante do que o princípio de separar a configuração do uso.

Última atualização significativa: 23 Jan 04

| Inglês (Original) | Chinês | Japonês |

Uma das coisas interessantes sobre o mundo do Java corporativo é o grande volume de atividade para o desenvolvimento de alternativas às tecnologias J2EE predominantes, boa parte dessas atividades com código aberto. Isto se deve, em parte, a uma reação à complexidade de boa parte do mundo J2EE, mas também se deve à exploração de alternativas e ao surgimento de idéias criativas. Um problema comum é como amarrar diferentes elementos: como encaixar esta arquitetura de controlador web com a interface de acesso ao banco de dados quando eles foram concebidos por times diferentes, com pouco conhecimento um do outro. Uma variedade de frameworks têm tentado resolver este problema, e muitos deles estão se expandindo para prover capacidade de amarrar componentes de diferentes camadas. Estes são freqüentemente referenciados como containers leves, e têm como exemplo o PicoContainer e o Spring .

Por trás destes containers há vários princípios interessantes de design, coisas que vão além, tanto de containers especificos quanto da plataforma Java. Aqui, eu quero iniciar a exploração de alguns destes princípios. Os exemplos que eu uso serão em Java, mas como a maior parte do que escrevo, os princípios podem ser aplicados igualmente em outros ambientes OO, particularmente o. Net.

Componentes e Serviços

O tópico da amarração dos componentes me traz quase que imediatamente a problemas de terminologia difícil que envolvem os termos serviço e componente. Pode-se achar facilmente artigos longos e contraditórios sobre estas definições. Para meus propósitos, abaixo estão os meus usos atuais destes sobrecarregados termos.

Eu uso ' componente ' para referenciar um agregado de software projetado para ser usado, sem modificação, por uma aplicação que está fora do controle dos criadores do componente. Por ' sem modificação ', eu quero dizer que a aplicação não altera o código-fonte dos componentes, apesar dela poder alterar o comportamento do componente extendendo-o de alguma maneira permitida pelos criadores do componentes.

Um ' serviço ' é similar ao componente, ao ponto que ele é utilizado por aplicações externas. A principal diferença é que eu espero que um componente seja usado localmente (pense em um arquivo JAR, DLL ou uma importação de código). Um serviço será usado remotamente, através de alguma interface remota, que pode ser tanto síncrona quanto assíncrona (web service, sistema de mensagens, RPC ou socket).

Na maioria das vezes, eu uso ' serviço ' neste artigo, mas muito da mesma lógica pode também ser aplicada a componentes locais. De fato, freqüentemente você precisa de algum tipo de framework de componentes locais para acessar facilmente um serviço remoto. Mas escrever ' componente ou serviço ' é cansativo para ler e escrever, e serviços estão bem mais na moda no momento.

Um Exemplo Simples

Para ajudar a tornar isso tudo mais concreto, eu usarei um exemplo funcional. Como todos os meus exemplos, este é um daqueles exemplos super-simples; pequeno o bastante para ser irreal, mas o suficiente, espero, para que se possa visualizar o que está ocorrendo sem cair na complexidade de um exemplo real.

Neste exemplo, estou criando um componente que fornece uma lista de filmes dirigidos por um determinado diretor. Esta função incrivelmente útil é implementada em um único método.

 

   class MovieLister...
   public Movie[] moviesDirectedBy(String arg) {
      List allMovies = finder.findAll();
      for (Iterator it = allMovies.iterator(); it.hasNext();) {
         Movie movie = (Movie) it.next();
          if (!movie.getDirector().equals(arg))
           
it.remove();
         }
      return (Movie[])allMovies.toArray(new Movie[allMovies.size()]);
   }

A implementação desta função é ingênua ao extremo. Ela pede ao finder (objeto de busca, no qual iremos nos aprofundar mais à frente) que este retorne todos os filmes que conhece. Então, é feita um percorrimento desta lista para retornar aqueles dirigidos por um diretor em particular. Este pedaço específico não será alterado posteriormente, já que ele é apenas uma ponte para o ponto real deste artigo.

O ponto principal deste artigo é o finder , mais particularmente como nós conectamos o lister (objeto de listagem) com um finder específico. Isso é interessante porque quero que o meu maravilhoso método moviesDirectedBy seja completamente independente de como todos os filmes são armazenados. Então, tudo o que o método faz é referenciar um finder e tudo o que o finder faz é saber como responder ao método findAll . Eu posso fazer isto definindo uma interface para o finder .

 public interface MovieFinder{ 
    List findAll() ;
}

Agora, tudo isto é muito bem desacoplado, mas em algum ponto eu tenho que obter uma classe concreta para realmente receber os filmes. Neste caso, eu pus este código no construtor da minha classe de listagem.

 class MovieLister...  
private MovieFinder finder;
  public MovieLister () {
   finder = new ColonDelimitedMovieFinder("movies1.txt");
}

O nome da classe de implementação vem do fato de que eu estou obtendo minha lista de um arquivo texto delimitado por caracteres ': ' (dois-pontos, colon, em inglês). Eu os pouparei dos detalhes, pois, de qualquer modo, é necessário apenas que haja alguma implementação.

Agora, se apenas eu mesmo estiver usando esta classe, está tudo bem. Mas o que acontece quando meus amigos estão loucos de vontade de ter esta incrível funcionalidade e gostariam de uma cópia do meu programa? Se eles também guardarem suas listagens de filmes em um arquivo texto delimitado por caracteres ': ' chamados " movies1.txt ", então está tudo uma maravilha. Se eles têm um nome diferente para seus arquivos de filmes fica fácil por o nome do deste arquivo em um outro arquivo de propriedades. Mas e se eles têm uma forma completamente diferente de armazenamento para sua lista de filmes: um banco de dados SQL, um arquivo XML, um web service, ou mesmo um arquivo texto em outro formato? Neste caso, nós precisamos de uma classe diferente para recuperar aqueles dados. Agora, como eu defini uma interface MovieFinder , isto não alterará meu método moviesDirectedBy . Mas eu ainda preciso de algum meio de obter uma instância da implementação do finder correto.

Figura 1: As dependências usando uma simples instanciação na classe de listagem.

A Figura 1 mostra as dependências para esta situação. A classe MovieLister é dependente tanto da interface MovieFinder quanto de sua implementação. Nós preferiríamos que esta fosse dependente apenas da interface, mas como podemos obter uma instância para se trabalhar?

Em meu livro P of EAA , descrevemos esta situação como um Plugin . A classe de implementação para o finder não é ligada ao programa em tempo de compilação, já que eu não sei o que meus amigos irão usar. Ao invés disto, queremos que meu objeto de listagem trabalhe com qualquer implementação, e que tal implementação seja plugada posteriormente, fora de meu controle. O problema é como eu posso fazer a ligação de modo que minha classe MovieLister não conheça a implementação, e mesmo assim consiga uma instância que mantenha o funcionamento.

Expandindo isto a um sistema real, nós podemos ter dúzias de tais serviços e componentes. Em cada caso, podemos abstrair nosso uso destes componentes interagindo com eles através de uma interface (e usando um adaptador, se o componente não foi projetado com uma interface em mente). Mas se nós desejarmos instalarmos o sistema de diferentes formas, precisamos usar plugins para lidar com a interação destes serviços, de modo que possamos usar diferentes implementações em diferentes instalações.

Então, o problema central é como podemos reunir estes plugins em uma aplicação? Este é um dos principais problemas que esta nova geração de containers leves tentam resolver e, de forma universal, todos eles o fazem utilizando Inversão de Controle.

Inversão de Controle

Quando as pessoas falam sobre como estes containers são tão úteis por implementarem " Inversão de Controle ", eu me sinto desconcertado. Inversão de controle é uma característica comum dos frameworks, portanto, dizer que estes containers leves são especiais porque usam inversão de controle é como dizer que meu carro é especial porque tem rodas.

A questão é, que aspecto de controle eles estão invertendo? Da primeira vez em que eu me deparei com inversão de controle, ela era o controle principal de uma interface com o usuário (UI). UIs antigas eram controladas por um programa de aplicação. Você tinha uma seqüência de comandos, como " entre com o nome ", " entre com o endereço "; seu programa direcionava as perguntas e lia a resposta de cada uma. Com as UIs gráficas, o framework de UI conteria o loop principal, e o seu programa proveria os tratadores de eventos para os vários campos da tela. O controle principal do programa foi invertido, movido para dentro do framework.

Para esta nova leva de containers, a inversão é sobre como eles procuram por uma implementação de um plugin. Em meu exemplo ingênuo, o objeto de listagem obtia a implementação do finder instanciando-a diretamente. Isto impede que o finder seja um plugin. A abordagem que estes containers usam é assegurar que qualquer usuário de um plugin siga alguma convenção que permita um módulo montador separado, responsável por injetar a implementação no objeto de listagem.

Assim, eu acho que nós precisamos de um nome mais específico para este padrão. Inversão de Controle é um termo genérico demais, deixando as pessoas confusas. Como um resultado de muita discussão com vários defensores da IoC, nós estabelecemos o nome Dependency Injection .

Eu começarei falando sobre as várias formas de Dependency Injection, mas friso agora que esta não é a única maneira de remover a dependência da classe aplicação quanto à implementação do plugin. Um outro padrão que pode ser usado é o Service Locator, que será discutido depois da explicação da Dependency Injection.

Formas de Dependency Injection

A idéia básica da Dependency Injection é ter um objeto separado, o montador (assembler), que popula um campo em um objeto lister com uma implementação apropriada para a interface finder , resultando em um diagrama de dependências parecido com a Figura 2 .

Figure 2: As dependências para um Injetor de Dependências

Existem três tipos principais de Dependency Injection. Os nomes que usarei são Constructor Injection (Injeção por Construtores), Setter Injection (Injeção por Métodos Set) e Interface Injection (Injeção por Interfaces). Se você ler sobre isto nas atuais discussões sobre Inversão de Controle, os verão sendo referenciados como IoC Tipo 1 (Interface Injection, IoC Tipo 2 (Setter Injection) e IoC Tipo 3 (Constructor Injection). Eu acho nomes numéricos particularmente difíceis de lembrar, por isso tenho usado os nomes definidos aqui.

Constructor Injection com o PicoContainer

Começarei mostrando como esta injeção é feita, usando-se um container leve chamado PicoContainer . Eu estou começando aqui, principalmente, porque muitos dos meus colegas na ThoughtWorks são muito ativos no desenvolvimento do PicoContainer (sim, é um tipo de nepotismo corporativo).

O PicoContainer usa um construtor para decidir como injetar uma implementação do finder na classe de listagem. Para isto funcionar, a classe de listagem de filmes precisa declarar um construtor que inclui tudo o que precisa ser injetado.

 class MovieLister...  
public MovieLister(MovieFinder finder) {
this.finder = finder;
}

O próprio finder irá, também, ser gerenciado pelo PicoContainer, e assim ter o nome do arquivo texto injetado pelo container.

 class ColonMovieFinder...  
public ColonMovieFinder(String filename) {
this.filename = filename;
}

O PicoContainer, então, precisa ser informado sobre que classe de implementação associar a cada interface, e que string injetar no finder .

 private MutablePicoContainer configureContainer() { 
MutablePicoContainer pico = new DefaultPicoContainer();
Parameter[] finderParams = { new ConstantParameter("movies1.txt")};
pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
pico.registerComponentImplementation(MovieLister.class);
return pico;
}

Este código de configuração é tipicamente feito em uma classe diferente. Por exemplo, cada amigo que usa meu lister pode escrever o código de configuração apropriado em alguma classe de setup própria. É claro que é comum manter este tipo de informação de configuração em arquivos de configuração separados. Pode-se escrever uma classe para ler um arquivo de configuração e ajustar o container apropriadamente. Embora o PicoContainer não contenha esta funcionalidade, existe um projeto relacionado, chamado NanoContainer, que provê os adaptadores apropriados para permitir que se tenham arquivos de configuração em XML. O NanoContainer interpretará o XML e então configurará o PicoContainer. A filosofia do projeto é separar o formato do arquivo de configuração do mecanismo interno.

Para usar o container, é necessário um código como este:

 public void testWithPico() { 
MutablePicoContainer pico = configureContainer();
MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);
Movie[] movies = lister.moviesDirectedBy(" Sergio Leone ");
assertEquals(" Once Upon a Time in the West " ,movies[0].getTitle()) ;
}

Embora neste exemplo eu tenha usado Constructor Injection, o PicoContainer também suporta Setter Injection, apesar de seus desenvolvedores preferirem o primeiro.

Setter Injection no Spring

O Spring é um framework abrangente voltado para o desenvolvimento Java corporativo. Ele inclui camadas de abstração para transações, frameworks de persistência, desenvolvimento de aplicações web e JDBC. Assim como o PicoContainer, ele suporta tanto Constructor quanto Setter Injection, mas seus desenvolvedores tender a preferir Setter Injection - o que o faz uma escolha apropriada para este exemplo.

Para fazer com que meu MovieLister aceite a injeção, eu defino um método set para este serviço:

 class MovieLister...  
private MovieFinder finder;
public void setFinder (MovieFinder finder) {
this.finder = finder;
}

De maneira semelhante, eu defino um método set para a string do buscador:

 class ColonMovieFinder...  
public void setFilename(String filename) {
this.filename = filename;
}

O terceiro passo é ajustar os arquivos de configuração. O Spring suporta tanto configuração por arquivos XML quanto por código, mas a maneira mais comum é usando XML.

 <beans> 
    <bean id = "MovieLister" class = "spring.MovieLister">
        <property name = "finder">
                <ref local = "MovieFinder" / >
        < / property>
    < / bean>
    <bean id = "MovieFinder" class = "spring.ColonMovieFinder">
        <property name = "filename">
                <value>movies1.txt< / value>
        < / property>
    < / bean>
< / beans>

O teste, então, fica assim.:

 

     public void testWithSpring() throws Exception{ 
ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");
MovieLister lister = (MovieLister) ctx.getBean(" MovieLister ");
Movie[] movies = lister.moviesDirectedBy("Sergio Leone") ;
assertEquals(" Once Upon a Time in the West ",movies[0].getTitle());
}

Interface Injection

A terceira técnica de injeção é definir e usar interfaces para este propósito. O Avalon é um exemplo de framework que usa esta técnica. Falarei mais sobre isto mais tarde, mas neste caso eu irei usá-lo com alguns exemplos de código bem simples.

Com esta técnica, eu começo definindo a interface que eu usarei para executar a injeção. Aqui está a interface para injetar um MovieFinder a um objeto:

 public interface InjectFinder{ 
void injectFinder ( MovieFinder finder );
}

Esta interface seria definida por qualquer um que fornecesse a interface MovieFinder. Ela precisa ser implementada por qualquer classe que queira usar um finder , como é o caso do lister :

 class MovieLister implements InjectFinder...  
public void injectFinder(MovieFinder finder) {
this.finder = finder;
}

Eu uso uma abordagem similar para injetar o nome de arquivo na implementação do finder :

public interface InjectFinderFilename {
void injectFilename ( String filename ) ;
}

class ColonMovieFinder implements MovieFinder,
InjectFinderFilename......

public void injectFilename ( String filename ) {
this . filename = filename;
}

Então, como sempre, eu preciso de algum código de configuração para amarrar as implementações.
Por simplicidade, eu o farei em código:

class Tester...
private Container container;

private void configureContainer () {
container = new Container () ;
registerComponents () ;
registerInjectors () ;
container.start () ;
}

Esta configuração tem dois estágios. O registro dos componentes com chaves identificadoras
é bem parecido com os outros exemplos:

class Tester...
private void registerComponents () {
container.registerComponent ( " MovieLister " , MovieLister. class ) ;
container.registerComponent ( " MovieFinder " , ColonMovieFinder. class ) ;
}

Um novo passo é registrar os injetores que irão injetar os componentes dependentes.
Cada interface de injeção precisa de algum código para injetar o objeto dependente.
Aqui, isto é feito registrando-se os objetos injetores no container.
Cada objeto injetor implementa a interface de injeção:

class Tester...
private void registerInjectors () {
container.registerInjector ( InjectFinder.class,
container.lookup
( " MovieFinder " )) ;
container.registerInjector ( InjectFinderFilename.class,
new FinderFilenameInjector ()) ;
}


public interface Injector {
public void inject ( Object target ) ;
}

Quando o dependente é uma classe escrita para este container, faz sendito para o componente que ele mesmo implemente uma interface de injeção, como faço aqui com o MovieFinder. Para classes genéricas, como uma string, eu uso uma inner class dentro do código de configuração:

class ColonMovieFinder implements Injector......
public void inject ( Object target ) {
(( InjectFinder ) target ) . injectFinder ( this ) ;
}


class Tester...
public static class FinderFilenameInjector implements Injector {
public void inject ( Object target ) {
(( InjectFinderFilename ) target ) . injectFilename ( " movies1.txt " ) ;
}
}

Os testes, então, utilizam o container:

class FinderFilenameInjector...
public void testIface () {
configureContainer () ;
MovieLister lister = ( MovieLister ) container.lookup ( " MovieLister " ) ;
Movie [] movies = lister.moviesDirectedBy ( " Sergio Leone " ) ;
assertEquals ( " Once Upon a Time in the West " , movies [ 0 ] . getTitle ()) ;
}

O container utiliza as interfaces de injeção declaradas para descobrir as dependências, e os injetores para injetar os dependentes corretos. (A implementação específica do container que eu fiz aqui não é importante para a técnica, e eu não a mostrarei, porque apenas causaria risos.)

Usando um Service Locator

O principal benefício da Dependency Injection é que ela remove a dependência que a classe MovieLister tem com a implementação concreta da MovieFinder . Isto me permite compartilhar as classes lister com amigos, e que eles possam plugar implementações apropriadas para seus ambientes. Dependency Injection não é a única maneira de quebrar esta dependência, outra que pode ser usada é um Service Locator .

A idéia básica por trás do Service Locator é ter um objeto que sabe como obter todos os serviços que uma aplicação pode ter. Então, um Service Locator para esta aplicação teria um método que retorna um MovieFinder quando este é necessário. Claro que isto apenas move esta carga um pouco, nós ainda temos que por o Locator dentro do lister , resultando nas dependências da Figura 3 :

Figura 3: As dependências para um Service Locator

Neste caso, usarei um ServiceLocator como um singleton como um Registry . O lister pode então usá-lo para obter o finder quando é instanciado.

class MovieLister...
MovieFinder finder = ServiceLocator.movieFinder () ;


class ServiceLocator...
public static MovieFinder movieFinder () {
return soleInstance.movieFinder;
}
private static ServiceLocator soleInstance;
private MovieFinder movieFinder;

Como na abordagem da Dependency Injection, nós temos que configurar o Service Locator.
Aqui, eu o farei em código, mas não é difícil usar um mecanismo que leria os dados apropriados de um arquivo de configuração:

class Tester...
private void configure () {
ServiceLocator.load ( new ServiceLocator (
new ColonMovieFinder ( " movies1.txt " ))
)
;
}


class ServiceLocator...
public static void load ( ServiceLocator arg ) {
soleInstance = arg;
}

public ServiceLocator ( MovieFinder movieFinder ) {
this . movieFinder = movieFinder;
}

Aqui está o código de teste:

class Tester...
public void testSimple () {
configure () ;
MovieLister lister = new MovieLister () ;
Movie [] movies = lister.moviesDirectedBy ( " Sergio Leone " ) ;
assertEquals ( " Once Upon a Time in the West " , movies [ 0 ] . getTitle ()) ;
}

Freqüentemente eu tenho ouvido a reclamação de que estes tipos de service locators são uma coisa ruim,
porque eles não são testáveis por não ser possível substituir suas implementações.
Certamente, se mal projetados eles podem levar a este tipo de problema, mas este não é necessariamente o caso.
A instância do service locator é apenas um simples guardador de dados. Podemos facilmente criar um locator com implementações de teste de nossos serviços.

Para um locator mais sofisticado, eu posso criar uma subclasse do servicelocator
e passá-la à variável de classe do registro. Eu posso alterar os métodos estáticos para chamar métodos na instância,
ao invés de de acessar diretamente as variáveis de instância. Posso prover locators específicos para uma thread,
usando um local de armazenamento específico para a thread. Tudo isto pode ser feito,
sem alterar os clientes do service locator.

Um modo de se pensar nisto é que o service locator é um registro, e não um singleton.
Um singleton provê uma maneira simples de implementar um registro, mas esta decisão
de implementação pode ser facilmente mudada.

Usando uma Interface Segregada para o Locator

Um dos problemas da abordagem simples acima é que o MovieLister é dependente da classe service locator completa,
mesmo que use apenas um serviço. Nós podemos reduzir isto usando uma interface segregada (separada).
Desta maneira, ao invés de usar a interface completa do service locator, o lister pode declarar apenas
a parte da interface de que precisa.

Assim, o fornecedor do MovieLister forneceria também uma interface do locator de que ele precisa para obter o finder :

public interface MovieFinderLocator {
public MovieFinder movieFinder () ;

O locator então precisa implementar esta interface para prover o aceso ao finder :

MovieFinderLocator locator = ServiceLocator.locator () ;
MovieFinder finder = locator.movieFinder () ;


public static ServiceLocator locator () {
return soleInstance;
}
public MovieFinder movieFinder () {
return movieFinder;
}
private static ServiceLocator soleInstance;
private MovieFinder movieFinder;

Note que, já que usamos uma interface, não temos mais acesso aos serviços por métodos estáticos.
Teremos que usar a classe para obter uma instância do locator, e então usá-la para obter o que precisarmos.

Um Service Locator Dinâmico

O exemplo acima era estático, quanto à classe do service locator ter métodos para cada um dos serviços.
Esta não é a única maneira de fazer isto, você pode também fazer um service locator dinâmico,
que permita que você agrupe nele qualquer serviço que você precise, e faça suas escolhas
em tempo de execução.

Neste caso, o service locator utiliza um mapa ao invés de campos para cada um dos serviços,
provendo métodos genéricos para obter e carregar serviços.

class ServiceLocator...
private static ServiceLocator soleInstance;
public static void load ( ServiceLocator arg ) {
soleInstance = arg;
}
private Map services = new HashMap () ;
public static Object getService ( String key ) {
return soleInstance.services.get ( key ) ;
}
public void loadService ( String key, Object service ) {
services.put ( key, service ) ;
}

A configuração envolve a carga do serviço com uma chave apropriada.

class Tester...
private void configure () {
ServiceLocator locator = new ServiceLocator () ;
locator.loadService ( " MovieFinder " , new ColonMovieFinder ( " movies1.txt " )) ;
ServiceLocator.load ( locator ) ;
}

Eu uso o serviço utilizando a mesma chave.

class MovieLister...
MovieFinder finder = ( MovieFinder ) ServiceLocator.getService ( " MovieFinder " ) ;

Em geral, eu não gosto desta abordagem. Apesar de que ela é realmente flexível, ela não é muito explícita.
A única maneira de se descobrir um serviço é através de chaves textuais. Eu prefiro métodos explícitos,
porque é mais fácil encontrar onde eles estão, olhando as definições da interface.

Utilizando tanto um locator quanto injeção com o Avalon

Dependency Injection e Service Locator não são necessariamente conceitos mutuamente excludentes.
Um bom exemplo é o uso dos dois juntos no framework Avalon. O Avalon utiliza um service locator,
mas usa injeção para informar os componentes onde encontrar o locator.

Berin Loritsch me enviou esta versão simples do meu exemplo usando Avalon.

public class MyMovieLister implements MovieLister, Serviceable {
private MovieFinder finder;

public void service ( ServiceManager manager ) throws ServiceException {
finder = ( MovieFinder ) manager.lookup ( " finder " ) ;

comentários: 0