Java Dependency Injection Entwurfsmuster ermöglicht es uns, die fest codierten Abhängigkeiten zu entfernen und unsere Anwendung locker gekoppelt, erweiterbar und wartbar zu machen. Wir können die Abhängigkeitsinjektion in Java implementieren, um die Abhängigkeitsauflösung von der Kompilierungszeit auf die Laufzeit zu verschieben.
Java Dependency Injection
Java Dependency Injection scheint in der Theorie schwer zu erfassen zu sein, daher werde ich ein einfaches Beispiel nehmen und dann sehen, wie man das Muster der Abhängigkeitsinjektion verwendet, um Lockerheit und Erweiterbarkeit in der Anwendung zu erreichen. Nehmen wir an, wir haben eine Anwendung, in der wir EmailService
verwenden, um E-Mails zu versenden. Normalerweise würden wir dies wie folgt implementieren.
package com.journaldev.java.legacy;
public class EmailService {
public void sendEmail(String message, String receiver){
// Logik zum Senden von E-Mails
System.out.println("Email sent to "+receiver+ " with Message="+message);
}
}
Die Klasse EmailService
enthält die Logik zum Senden einer E-Mail-Nachricht an die Empfänger-E-Mail-Adresse. Unser Anwendungscode würde wie folgt aussehen.
package com.journaldev.java.legacy;
public class MyApplication {
private EmailService email = new EmailService();
public void processMessages(String msg, String rec){
// Führen Sie einige Nachrichtenvalidierungs-, Manipulationslogik usw. aus
this.email.sendEmail(msg, rec);
}
}
Unser Clientcode, der die Klasse MyApplication
verwenden wird, um E-Mail-Nachrichten zu senden, sieht wie folgt aus.
package com.journaldev.java.legacy;
public class MyLegacyTest {
public static void main(String[] args) {
MyApplication app = new MyApplication();
app.processMessages("Hi Pankaj", "[email protected]");
}
}
Auf den ersten Blick scheint mit der obigen Implementierung nichts falsch zu sein. Aber der obige Code hat bestimmte Einschränkungen.
- Die Klasse
MyApplication
ist dafür verantwortlich, den E-Mail-Dienst zu initialisieren und ihn dann zu verwenden. Dies führt zu einer fest codierten Abhängigkeit. Wenn wir in Zukunft zu einem anderen fortschrittlichen E-Mail-Dienst wechseln möchten, erfordert dies Codeänderungen in der KlasseMyApplication
. Dies erschwert die Erweiterung unserer Anwendung, und wenn der E-Mail-Dienst in mehreren Klassen verwendet wird, wird dies noch schwieriger. - Wenn wir unsere Anwendung erweitern möchten, um eine zusätzliche Messaging-Funktion wie SMS oder Facebook-Nachrichten bereitzustellen, müssten wir eine weitere Anwendung dafür schreiben. Dies erfordert Codeänderungen in Anwendungsklassen und auch in Clientklassen.
- Das Testen der Anwendung wird sehr schwierig sein, da unsere Anwendung direkt die E-Mail-Dienstinstanz erstellt. Es gibt keine Möglichkeit, diese Objekte in unseren Testklassen zu mocken.
Man könnte argumentieren, dass wir die Erstellung der E-Mail-Dienstinstanz aus der Klasse MyApplication
entfernen können, indem wir einen Konstruktor haben, der den E-Mail-Dienst als Argument erfordert.
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){
//do some msg validation, manipulation logic etc
this.email.sendEmail(msg, rec);
}
}
Aber in diesem Fall fordern wir Clientanwendungen oder Testklassen auf, den E-Mail-Dienst zu initialisieren, was keine gute Designentscheidung ist. Jetzt sehen wir uns an, wie wir das Java-Abhängigkeitsinjektionsmuster anwenden können, um alle Probleme mit der obigen Implementierung zu lösen. Die Dependency Injection in Java erfordert mindestens Folgendes:
- Die Servicekomponenten sollten mit einer Basisklasse oder einem Interface entworfen werden. Es ist besser, Schnittstellen oder abstrakte Klassen zu bevorzugen, die den Vertrag für die Dienste definieren würden.
- Die Verbraucherklassen sollten in Bezug auf das Service-Interface geschrieben werden.
- Injector-Klassen, die die Dienste initialisieren, und dann die Verbraucherklassen.
Java Dependency Injection – Service-Komponenten
Für unseren Fall können wir eine MessageService
haben, die den Vertrag für Dienstimplementierungen deklariert.
package com.journaldev.java.dependencyinjection.service;
public interface MessageService {
void sendMessage(String msg, String rec);
}
Angenommen, wir haben E-Mail- und SMS-Dienste, die die obigen Schnittstellen implementieren.
package com.journaldev.java.dependencyinjection.service;
public class EmailServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
// Logik zum Senden von E-Mails
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) {
// Logik zum Senden von SMS
System.out.println("SMS sent to "+rec+ " with Message="+msg);
}
}
Unsere Dependency Injection Java-Dienste sind bereit, und jetzt können wir unsere Verbraucherklasse schreiben.
Java Dependency Injection – Service-Verbraucher
Es ist nicht erforderlich, Basisschnittstellen für Verbraucherklassen zu haben, aber ich werde eine Consumer
-Schnittstelle haben, die den Vertrag für Verbraucherklassen deklariert.
package com.journaldev.java.dependencyinjection.consumer;
public interface Consumer {
void processMessages(String msg, String rec);
}
Die Implementierung meiner Verbraucherklasse sieht wie folgt aus.
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){
// Führe einige Nachrichtenvalidierungs- und Manipulationslogik durch, etc.
this.service.sendMessage(msg, rec);
}
}
Beachten Sie, dass unsere Anwendungs-Klasse lediglich den Service verwendet. Sie initialisiert den Service nicht, was zu einer besseren „Trennung der Anliegen“ führt. Die Verwendung einer Service-Schnittstelle ermöglicht es uns auch, die Anwendung leicht zu testen, indem wir den MessageService mocken und die Services zur Laufzeit anstatt zur Kompilierungszeit binden. Jetzt sind wir bereit, Java-Abhängigkeitsinjektor-Klassen zu schreiben, die den Service initialisieren und auch die Verbraucher-Klassen.
Java-Abhängigkeitsinjektion – Injektor-Klassen
Wir erstellen ein Interface „MessageServiceInjector“ mit einer Methodendeklaration, die die Klasse „Consumer“ zurückgibt.
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
public interface MessageServiceInjector {
public Consumer getConsumer();
}
Für jeden Service müssen wir nun Injektor-Klassen wie unten gezeigt erstellen.
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());
}
}
Nun sehen wir, wie unsere Client-Anwendungen die Anwendung mit einem einfachen Programm verwenden.
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;
//E-Mail senden
injector = new EmailServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, email);
//SMS senden
injector = new SMSServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, phone);
}
}
Wie Sie sehen können, sind unsere Anwendungsklassen nur dafür verantwortlich, den Service zu verwenden. Die Service-Klassen werden in den Injectors erstellt. Wenn wir unsere Anwendung weiter ausbauen möchten, um beispielsweise das Versenden von Facebook-Nachrichten zu ermöglichen, müssen wir nur Service-Klassen und Injektor-Klassen schreiben. Die Implementierung der Abhängigkeitsinjektion löst also das Problem mit fest codierten Abhängigkeiten und macht unsere Anwendung flexibel und leicht erweiterbar. Nun sehen wir, wie einfach wir unsere Anwendungsklasse testen können, indem wir den Injektor und die Service-Klassen mocken.
Java-Abhängigkeitsinjektion – JUnit-Testfall mit Mock-Injector und Service
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(){
// den Injector mit anonymer Klasse mocken
injector = new MessageServiceInjector() {
@Override
public Consumer getConsumer() {
// den Nachrichtendienst mocken
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;
}
}
Wie Sie sehen können, verwende ich anonyme Klassen, um den Injector und die Serviceklassen zu mocken, und ich kann problemlos die Methoden meiner Anwendung testen. Ich verwende JUnit 4 für die obige Testklasse, also stellen Sie sicher, dass sie im Build-Pfad Ihres Projekts liegt, wenn Sie die obige Testklasse ausführen. Wir haben Konstruktoren verwendet, um die Abhängigkeiten in den Anwendungsklassen zu injizieren. Eine andere Möglichkeit besteht darin, eine Setter-Methode zu verwenden, um Abhängigkeiten in Anwendungsklassen zu injizieren. Für die Injektion von Abhängigkeiten über Setter-Methoden wird unsere Anwendungsklasse wie folgt implementiert.
package com.journaldev.java.dependencyinjection.consumer;
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplication implements Consumer{
private MessageService service;
public MyDIApplication(){}
// Setter-Abhängigkeitsinjektion
public void setService(MessageService service) {
this.service = service;
}
@Override
public void processMessages(String msg, String rec){
// einige Nachrichtenvalidierungs-, Manipulationslogik usw. durchführen
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;
}
}
Eines der besten Beispiele für die Injektion von Setter-Abhängigkeiten ist Struts2 Servlet API Aware Interfaces. Ob die Konstruktor-basierte oder die Setter-basierte Abhängigkeitsinjektion verwendet werden soll, ist eine Designentscheidung und hängt von Ihren Anforderungen ab. Wenn beispielsweise meine Anwendung überhaupt nicht ohne die Serviceklasse funktionieren kann, würde ich die konstruktorbasierte DI bevorzugen, sonst würde ich die settermethodenbasierte DI verwenden, um sie nur dann zu verwenden, wenn es wirklich nötig ist. Dependency Injection in Java ist eine Möglichkeit, die Umkehrung der Steuerung (IoC) in unserer Anwendung zu erreichen, indem Objektbindungen von der Kompilierzeit zur Laufzeit verschoben werden. Wir können IoC auch durch Factory Pattern, Template Method Design Pattern, Strategy Pattern und Service Locator Pattern erreichen. Die Frameworks Spring Dependency Injection, Google Guice und Java EE CDI erleichtern den Prozess der Abhängigkeitsinjektion durch die Verwendung von Java Reflection API und Java-Annotationen. Alles, was wir tun müssen, ist das Feld, den Konstruktor oder die Setter-Methode zu annotieren und sie in Konfigurations-XML-Dateien oder Klassen zu konfigurieren.
Vorteile der Java-Abhängigkeitsinjektion
Einige der Vorteile der Verwendung von Dependency Injection in Java sind:
- Trennung der Anliegen
- Reduzierung von Boilerplate-Code in Anwendungs-Klassen, da alle Arbeiten zur Initialisierung von Abhängigkeiten vom Injektor-Komponenten übernommen werden
- Konfigurierbare Komponenten machen die Anwendung leicht erweiterbar
- Unit-Tests sind einfach mit Mock-Objekten
Nachteile der Java-Abhängigkeitsinjektion
Java-Abhängigkeitsinjektion hat auch einige Nachteile:
- Bei übermäßiger Verwendung kann es zu Wartungsproblemen führen, da die Auswirkungen von Änderungen erst zur Laufzeit bekannt sind.
- Dependency Injection in Java verbirgt die Serviceklassenabhängigkeiten, was zu Laufzeitfehlern führen kann, die zur Kompilierzeit erkannt worden wären.
Dependency Injection Projekt herunterladen
Das ist alles zum Dependency Injection-Muster in Java. Es ist gut, es zu kennen und zu verwenden, wenn wir die Kontrolle über die Dienste haben.