Il design pattern di Java Dependency Injection ci permette di rimuovere le dipendenze codificate duramente e rendere la nostra applicazione debolmente accoppiata, estendibile e manutenibile. Possiamo implementare l’iniezione di dipendenze in Java per spostare la risoluzione delle dipendenze dall’ora di compilazione all’ora di esecuzione.
L’iniezione di dipendenze in Java
L’iniezione di dipendenze in Java sembra difficile da comprendere solo con la teoria, quindi prenderei un esempio semplice e poi vedremo come utilizzare il pattern di iniezione di dipendenze per ottenere un accoppiamento debole e un’estendibilità nell’applicazione. Diciamo che abbiamo un’applicazione in cui consumiamo EmailService
per inviare email. Normalmente implementeremmo questo come segue.
package com.journaldev.java.legacy;
public class EmailService {
public void sendEmail(String message, String receiver){
// logica per inviare email
System.out.println("Email sent to "+receiver+ " with Message="+message);
}
}
La classe EmailService
contiene la logica per inviare un messaggio email all’indirizzo email del destinatario. Il nostro codice dell’applicazione sarà come segue.
package com.journaldev.java.legacy;
public class MyApplication {
private EmailService email = new EmailService();
public void processMessages(String msg, String rec){
// eseguire alcune logiche di validazione del messaggio, manipolazione, ecc.
this.email.sendEmail(msg, rec);
}
}
Il nostro codice client che utilizzerà la classe MyApplication
per inviare messaggi email sarà come segue.
package com.journaldev.java.legacy;
public class MyLegacyTest {
public static void main(String[] args) {
MyApplication app = new MyApplication();
app.processMessages("Hi Pankaj", "[email protected]");
}
}
A prima vista, sembra non ci sia nulla di sbagliato nell’implementazione sopra. Ma la logica del codice sopra ha certe limitazioni.
- La classe
MyApplication
è responsabile dell’inizializzazione del servizio email e del suo utilizzo. Questo porta a una dipendenza codificata duramente. Se volessimo passare a un altro servizio email più avanzato in futuro, richiederebbe modifiche al codice nella classe MyApplication. Ciò rende difficile estendere la nostra applicazione e se il servizio email viene utilizzato in più classi, sarebbe ancora più difficile. - Se volessimo estendere la nostra applicazione per fornire una funzionalità di messaggistica aggiuntiva, come SMS o messaggio su Facebook, dovremmo scrivere un’altra applicazione per questo. Ciò comporterà modifiche al codice nelle classi dell’applicazione e nelle classi client.
- Testare l’applicazione sarà molto difficile poiché la nostra applicazione crea direttamente l’istanza del servizio email. Non c’è modo di simulare questi oggetti nelle nostre classi di test.
Qualcuno potrebbe sostenere che possiamo rimuovere la creazione dell’istanza del servizio email dalla classe MyApplication
avendo un costruttore che richiede il servizio email come argomento.
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){
// esegui alcune operazioni di validazione dei messaggi, logica di manipolazione, ecc.
this.email.sendEmail(msg, rec);
}
}
Ma in questo caso, stiamo chiedendo alle applicazioni client o alle classi di test di inizializzare il servizio email, che non è una buona decisione di progettazione. Ora vediamo come possiamo applicare il pattern di injection delle dipendenze di Java per risolvere tutti i problemi con l’implementazione sopra. L’iniezione delle dipendenze in Java richiede almeno quanto segue:
- I componenti del servizio dovrebbero essere progettati con una classe di base o un’interfaccia. È meglio preferire interfacce o classi astratte che definirebbero il contratto per i servizi.
- Le classi consumatrici dovrebbero essere scritte in termini di interfaccia del servizio.
- Classi di iniezione che inizieranno i servizi e poi le classi consumer.
Java Dependency Injection – Componenti del servizio
Per il nostro caso, possiamo avere MessageService
che dichiarerà il contratto per le implementazioni del servizio.
package com.journaldev.java.dependencyinjection.service;
public interface MessageService {
void sendMessage(String msg, String rec);
}
Supponiamo ora di avere servizi di posta elettronica (Email) e SMS che implementano le interfacce sopra.
package com.journaldev.java.dependencyinjection.service;
public class EmailServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
//logica per inviare 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) {
//logica per inviare SMS
System.out.println("SMS sent to "+rec+ " with Message="+msg);
}
}
I nostri servizi di iniezione delle dipendenze in Java sono pronti e ora possiamo scrivere la nostra classe consumer.
Java Dependency Injection – Consumatore di servizi
Non è necessario avere interfacce di base per le classi consumer, ma avrò un’interfaccia Consumer
che dichiara il contratto per le classi consumer.
package com.journaldev.java.dependencyinjection.consumer;
public interface Consumer {
void processMessages(String msg, String rec);
}
La mia implementazione della classe consumer è come segue.
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){
//esegui alcune validazioni di messaggi, logica di manipolazione, ecc.
this.service.sendMessage(msg, rec);
}
}
Si noti che la nostra classe di applicazione sta solo utilizzando il servizio. Non inizializza il servizio che porta a una migliore “separazione delle preoccupazioni“. Inoltre, l’uso dell’interfaccia del servizio ci consente di testare facilmente l’applicazione simulando il MessageService e collegando i servizi a tempo di esecuzione anziché a tempo di compilazione. Ora siamo pronti a scrivere classi di iniezione delle dipendenze di Java che inizieranno il servizio e anche le classi consumer.
Iniezione delle Dipendenze in Java – Classi degli Iniettori
Abbiamo un’interfaccia MessageServiceInjector
con la dichiarazione del metodo che restituisce la classe Consumer
.
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
public interface MessageServiceInjector {
public Consumer getConsumer();
}
Ora per ogni servizio, dovremo creare classi iniettori come segue.
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());
}
}
Ora vediamo come le nostre applicazioni client utilizzeranno l’applicazione con un programma semplice.
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;
//Invia email
injector = new EmailServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, email);
//Invia SMS
injector = new SMSServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, phone);
}
}
Come si può vedere, le nostre classi di applicazione sono responsabili solo dell’utilizzo del servizio. Le classi di servizio sono create negli iniettori. Inoltre, se dovessimo estendere ulteriormente la nostra applicazione per consentire la messaggistica su Facebook, dovremmo scrivere solo classi di servizio e iniettori. Quindi l’implementazione dell’iniezione delle dipendenze ha risolto il problema con la dipendenza codificata duramente e ci ha aiutato a rendere la nostra applicazione flessibile e facile da estendere. Ora vediamo quanto sia facile testare la nostra classe di applicazione simulando l’iniettore e le classi di servizio.
Iniezione di dipendenze Java – Caso di test JUnit con Iniettore e Servizio Fittizio
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(){
// simulare l'iniettore con una classe anonima
injector = new MessageServiceInjector() {
@Override
public Consumer getConsumer() {
// simulare il servizio di messaggistica
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;
}
}
Come puoi vedere, sto utilizzando classi anonime per simulare le classi dell’iniettore e del servizio e posso testare facilmente i metodi della mia applicazione. Sto utilizzando JUnit 4 per la classe di test sopra, quindi assicurati che sia nel percorso di compilazione del tuo progetto se stai eseguendo la classe di test sopra. Abbiamo utilizzato i costruttori per iniettare le dipendenze nelle classi dell’applicazione, un altro modo è utilizzare un metodo setter per iniettare le dipendenze nelle classi dell’applicazione. Per l’iniezione di dipendenze tramite il metodo setter, la nostra classe di applicazione sarà implementata come segue.
package com.journaldev.java.dependencyinjection.consumer;
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplication implements Consumer{
private MessageService service;
public MyDIApplication(){}
// iniezione di dipendenze tramite setter
public void setService(MessageService service) {
this.service = service;
}
@Override
public void processMessages(String msg, String rec){
// fare qualche logica di validazione del messaggio, manipolazione, ecc.
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;
}
}
Uno degli esempi migliori di iniezione di dipendenza tramite setter è Struts2 Servlet API Aware interfaces. Scegliere tra l’iniezione di dipendenza basata su costruttore o su setter è una decisione di progettazione e dipende dalle tue esigenze. Ad esempio, se la mia applicazione non può funzionare affatto senza la classe di servizio, allora preferirei l’iniezione di dipendenza basata su costruttore, altrimenti opterei per l’iniezione di dipendenza basata sul metodo setter da usare solo quando è veramente necessario. Dependency Injection in Java è un modo per ottenere Inversion of control (IoC) nella nostra applicazione spostando il legame degli oggetti dal tempo di compilazione al tempo di esecuzione. Possiamo ottenere IoC attraverso Factory Pattern, Template Method Design Pattern, Strategy Pattern e anche il pattern Service Locator. Spring Dependency Injection, Google Guice e i framework Java EE CDI facilitano il processo di iniezione di dipendenza attraverso l’uso di Java Reflection API e annotazioni java. Tutto ciò che dobbiamo fare è annotare il campo, il costruttore o il metodo setter e configurarli nei file xml di configurazione o nelle classi.
Vantaggi dell’Iniezione di Dipendenze in Java
Alcuni dei vantaggi dell’utilizzo dell’Iniezione di Dipendenze in Java sono:
- Separazione delle Preoccupazioni
- Riduzione del codice di base nelle classi dell’applicazione poiché tutto il lavoro per inizializzare le dipendenze è gestito dal componente di iniezione
- I componenti configurabili rendono l’applicazione facilmente estendibile
- La testabilità unitaria è semplice con oggetti mock
Svantaggi dell’Iniezione di Dipendenze in Java
L’Iniezione di Dipendenze in Java ha anche alcuni svantaggi:
- Se usata in modo eccessivo, può portare a problemi di manutenzione perché gli effetti delle modifiche sono noti solo a tempo di esecuzione
- L’iniezione di dipendenze in Java nasconde le dipendenze della classe di servizio che possono portare a errori di esecuzione che sarebbero stati rilevati a tempo di compilazione
Scarica il Progetto di Iniezione di Dipendenze
Ecco tutto per il pattern di iniezione di dipendenze in Java. È buono saperlo e usarlo quando siamo noi a controllare i servizi.