Injeção de Dependência em Java permite-nos remover as dependências codificadas e tornar nossa aplicação fracamente acoplada, extensível e mantível. Podemos implementar a injeção de dependência em Java para mover a resolução de dependências do tempo de compilação para o tempo de execução.
Injeção de Dependência em Java
A injeção de dependência em Java parece difícil de entender apenas com a teoria, então eu vou dar um exemplo simples e depois veremos como usar o padrão de injeção de dependência para alcançar um acoplamento fraco e extensibilidade na aplicação. Vamos supor que temos uma aplicação onde consumimos o EmailService
para enviar e-mails. Normalmente, implementaríamos isso da seguinte forma.
package com.journaldev.java.legacy;
public class EmailService {
public void sendEmail(String message, String receiver){
// lógica para enviar e-mail
System.out.println("Email sent to "+receiver+ " with Message="+message);
}
}
A classe EmailService
contém a lógica para enviar uma mensagem de e-mail para o endereço de e-mail do destinatário. Nosso código da aplicação será assim.
package com.journaldev.java.legacy;
public class MyApplication {
private EmailService email = new EmailService();
public void processMessages(String msg, String rec){
// fazer alguma validação de mensagem, lógica de manipulação, etc
this.email.sendEmail(msg, rec);
}
}
Nosso código cliente que usará a classe MyApplication
para enviar mensagens de e-mail será assim.
package com.journaldev.java.legacy;
public class MyLegacyTest {
public static void main(String[] args) {
MyApplication app = new MyApplication();
app.processMessages("Hi Pankaj", "[email protected]");
}
}
Ao primeiro olhar, não parece haver nada de errado com a implementação acima. Mas a lógica de código acima tem certas limitações.
- A classe
MyApplication
é responsável por inicializar o serviço de e-mail e então usá-lo. Isso leva a uma dependência codificada. Se quisermos mudar para algum outro serviço de e-mail avançado no futuro, será necessário fazer alterações no código na classeMyApplication
. Isso torna nossa aplicação difícil de estender e, se o serviço de e-mail for usado em várias classes, isso seria ainda mais difícil. - Se quisermos estender nossa aplicação para fornecer um recurso de mensagens adicional, como SMS ou mensagem no Facebook, então precisaríamos escrever outra aplicação para isso. Isso envolverá alterações de código nas classes da aplicação e também nas classes do cliente.
- Testar a aplicação será muito difícil, já que nossa aplicação está criando diretamente a instância do serviço de e-mail. Não há como simular esses objetos em nossas classes de teste.
Alguém pode argumentar que podemos remover a criação da instância do serviço de e-mail da classe MyApplication
tendo um construtor que requer o serviço de e-mail como argumento.
package com.journaldev.java.legacy;
public class MyApplication {
private EmailService email = null;
public MyApplication(EmailService svc){
this.email=svc;
}
public void processMessages(String msg, String rec){
// faça alguma validação de mensagem, lógica de manipulação etc
this.email.sendEmail(msg, rec);
}
}
Mas, neste caso, estamos pedindo que as aplicações cliente ou classes de teste inicializem o serviço de e-mail, o que não é uma boa decisão de design. Agora, vamos ver como podemos aplicar o padrão de injeção de dependência em Java para resolver todos os problemas com a implementação acima. A injeção de dependência em Java requer pelo menos o seguinte:
- Os componentes de serviço devem ser projetados com uma classe base ou interface. É melhor preferir interfaces ou classes abstratas que definiriam o contrato para os serviços.
- As classes consumidoras devem ser escritas em termos da interface do serviço.
- Classes de injetor que inicializarão os serviços e então as classes consumidoras.
Java Dependency Injection – Componentes de Serviço
Para o nosso caso, podemos ter MessageService
que declarará o contrato para implementações de serviço.
package com.journaldev.java.dependencyinjection.service;
public interface MessageService {
void sendMessage(String msg, String rec);
}
Agora, vamos dizer que temos serviços de Email e SMS que implementam as interfaces acima.
package com.journaldev.java.dependencyinjection.service;
public class EmailServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
// lógica para enviar email
System.out.println("Email sent to "+rec+ " with Message="+msg);
}
}
package com.journaldev.java.dependencyinjection.service;
public class SMSServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
// lógica para enviar SMS
System.out.println("SMS sent to "+rec+ " with Message="+msg);
}
}
Nossos serviços de injeção de dependência Java estão prontos e agora podemos escrever nossa classe consumidora.
Java Dependency Injection – Consumidor de Serviço
Não é necessário ter interfaces base para classes consumidoras, mas eu terei uma interface Consumer
declarando contrato para classes consumidoras.
package com.journaldev.java.dependencyinjection.consumer;
public interface Consumer {
void processMessages(String msg, String rec);
}
A implementação da minha classe consumidora é como abaixo.
package com.journaldev.java.dependencyinjection.consumer;
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplication implements Consumer{
private MessageService service;
public MyDIApplication(MessageService svc){
this.service=svc;
}
@Override
public void processMessages(String msg, String rec){
// fazer alguma validação de mensagem, lógica de manipulação, etc.
this.service.sendMessage(msg, rec);
}
}
Note que nossa classe de aplicação está apenas usando o serviço. Ela não inicializa o serviço, o que leva a uma melhor “separação de preocupações”. Além disso, o uso da interface do serviço nos permite testar facilmente a aplicação ao simular o MessageService e vincular os serviços em tempo de execução, em vez de tempo de compilação. Agora estamos prontos para escrever classes de injeção de dependência em Java que inicializarão o serviço e também as classes consumidoras.
Injeção de Dependência em Java – Classes Injetoras
Vamos ter uma interface MessageServiceInjector
com declaração de método que retorna a classe Consumer
.
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
public interface MessageServiceInjector {
public Consumer getConsumer();
}
Agora, para cada serviço, teremos que criar classes injetoras como abaixo.
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.EmailServiceImpl;
public class EmailServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
return new MyDIApplication(new EmailServiceImpl());
}
}
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.SMSServiceImpl;
public class SMSServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
return new MyDIApplication(new SMSServiceImpl());
}
}
Agora vejamos como nossas aplicações cliente usarão a aplicação com um programa simples.
package com.journaldev.java.dependencyinjection.test;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.injector.EmailServiceInjector;
import com.journaldev.java.dependencyinjection.injector.MessageServiceInjector;
import com.journaldev.java.dependencyinjection.injector.SMSServiceInjector;
public class MyMessageDITest {
public static void main(String[] args) {
String msg = "Hi Pankaj";
String email = "[email protected]";
String phone = "4088888888";
MessageServiceInjector injector = null;
Consumer app = null;
//Enviar email
injector = new EmailServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, email);
//Enviar SMS
injector = new SMSServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, phone);
}
}
Como você pode ver, nossas classes de aplicação são responsáveis apenas por usar o serviço. As classes de serviço são criadas nas injetoras. Além disso, se precisarmos estender ainda mais nossa aplicação para permitir mensagens do Facebook, teremos que escrever apenas classes de serviço e classes injetoras. Portanto, a implementação de injeção de dependência resolveu o problema com dependência codificada e nos ajudou a tornar nossa aplicação flexível e fácil de estender. Agora, vejamos como podemos testar facilmente nossa classe de aplicação ao simular as classes injetora e de serviço.
Injeção de Dependência em Java – Caso de Teste JUnit com Injetor e Serviço Falsificados
package com.journaldev.java.dependencyinjection.test;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.injector.MessageServiceInjector;
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplicationJUnitTest {
private MessageServiceInjector injector;
@Before
public void setUp(){
//falsifique o injetor com uma classe anônima
injector = new MessageServiceInjector() {
@Override
public Consumer getConsumer() {
//falsifique o serviço de mensagem
return new MyDIApplication(new MessageService() {
@Override
public void sendMessage(String msg, String rec) {
System.out.println("Mock Message Service implementation");
}
});
}
};
}
@Test
public void test() {
Consumer consumer = injector.getConsumer();
consumer.processMessages("Hi Pankaj", "[email protected]");
}
@After
public void tear(){
injector = null;
}
}
Como você pode ver, estou usando classes anônimas para falsificar as classes do injetor e do serviço e posso testar facilmente os métodos da minha aplicação. Estou usando JUnit 4 para a classe de teste acima, então certifique-se de que está no caminho de construção do seu projeto se estiver executando a classe de teste acima. Usamos construtores para injetar as dependências nas classes da aplicação, outra maneira é usar um método setter para injetar dependências nas classes da aplicação. Para injeção de dependência por método setter, nossa classe de aplicação será implementada como abaixo.
package com.journaldev.java.dependencyinjection.consumer;
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplication implements Consumer{
private MessageService service;
public MyDIApplication(){}
//injeção de dependência por setter
public void setService(MessageService service) {
this.service = service;
}
@Override
public void processMessages(String msg, String rec){
//faça alguma validação de mensagem, lógica de manipulação etc.
this.service.sendMessage(msg, rec);
}
}
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.EmailServiceImpl;
public class EmailServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
MyDIApplication app = new MyDIApplication();
app.setService(new EmailServiceImpl());
return app;
}
}
Um dos melhores exemplos de injeção de dependência por setter é o Struts2 Servlet API Aware interfaces. A decisão de usar injeção de dependência baseada em construtor ou por setter é uma escolha de design e depende dos seus requisitos. Por exemplo, se minha aplicação não puder funcionar de forma alguma sem a classe de serviço, eu preferiria DI baseada em construtor, caso contrário, optaria por DI baseada em método setter para usá-la apenas quando realmente necessário. A Injeção de Dependência em Java é uma maneira de alcançar a Inversão de Controle (IoC) em nossa aplicação, movendo a vinculação de objetos do tempo de compilação para o tempo de execução. Podemos alcançar IoC por meio do Padrão de Fábrica, Padrão de Design Template Method, Padrão de Design Strategy e também o padrão Service Locator. Injeção de Dependência no Spring, Google Guice e o framework Java EE CDI facilitam o processo de injeção de dependência por meio do uso do Java Reflection API e anotações Java. Tudo o que precisamos fazer é anotar o campo, construtor ou método setter e configurá-los em arquivos XML de configuração ou classes.
Vantagens da Injeção de Dependência em Java
Alguns dos benefícios de usar Injeção de Dependência em Java são:
- Separation of Concerns
- Redução do código boilerplate nas classes de aplicação, pois todo o trabalho para inicializar dependências é tratado pelo componente injetor
- Componentes configuráveis tornam a aplicação facilmente extensível
- Teste unitário é fácil com objetos simulados
Desvantagens da Injeção de Dependência em Java
A injeção de dependência em Java também possui algumas desvantagens:
- Se usada em excesso, pode levar a problemas de manutenção, pois o efeito das mudanças só é conhecido em tempo de execução.
- A injeção de dependência em Java oculta as dependências da classe de serviço, o que pode resultar em erros em tempo de execução que teriam sido capturados em tempo de compilação.
Download do Projeto de Injeção de Dependência
Isso é tudo para o padrão de injeção de dependência em Java. É bom conhecer e usá-lo quando temos controle sobre os serviços.