Das Prinzip der Abhängigkeitsinversion in Java

1. Übersicht

Das Dependency Inversion Principle (DIP) ist Teil der Sammlung objektorientierter Programmierprinzipien, die im Volksmund als SOLID bekannt sind.

Das DIP ist ein einfaches, aber leistungsstarkes Programmierparadigma, mit dem wir gut strukturierte, stark entkoppelte und wiederverwendbare Softwarekomponenten implementieren können .

In diesem Tutorial werden verschiedene Ansätze zur Implementierung des DIP untersucht - einer in Java 8 und einer in Java 11 unter Verwendung des JPMS (Java Platform Module System).

2. Abhängigkeitsinjektion und Inversion der Steuerung sind keine DIP-Implementierungen

Lassen Sie uns zuallererst eine grundlegende Unterscheidung treffen, um die Grundlagen richtig zu machen: Das DIP ist weder eine Abhängigkeitsinjektion (DI) noch eine Inversion der Steuerung (IoC) . Trotzdem arbeiten sie alle großartig zusammen.

Einfach ausgedrückt geht es bei DI darum, Softwarekomponenten so zu gestalten, dass sie ihre Abhängigkeiten oder Mitarbeiter explizit über ihre APIs deklarieren, anstatt sie selbst zu erwerben.

Ohne DI sind Softwarekomponenten eng miteinander verbunden. Daher sind sie schwer wiederzuverwenden, zu ersetzen, zu verspotten und zu testen, was zu starren Designs führt.

Mit DI wird die Verantwortung für die Bereitstellung der Komponentenabhängigkeiten und Verdrahtungsobjektdiagramme von den Komponenten auf das zugrunde liegende Injektionsframework übertragen. Aus dieser Perspektive ist DI nur ein Weg, um IoC zu erreichen.

Andererseits ist IoC ein Muster, bei dem die Steuerung des Ablaufs einer Anwendung umgekehrt wird . Mit herkömmlichen Programmiermethoden hat unser benutzerdefinierter Code die Kontrolle über den Ablauf einer Anwendung. Umgekehrt wird bei IoC die Steuerung auf ein externes Framework oder einen externen Container übertragen .

Das Framework ist eine erweiterbare Codebasis, die Hook-Punkte zum Einfügen unseres eigenen Codes definiert .

Das Framework ruft wiederum unseren Code über eine oder mehrere spezialisierte Unterklassen, mithilfe von Schnittstellenimplementierungen und über Anmerkungen zurück. Das Spring-Framework ist ein schönes Beispiel für diesen letzten Ansatz.

3. Grundlagen von DIP

Um die Motivation hinter dem DIP zu verstehen, beginnen wir mit seiner formalen Definition, die Robert C. Martin in seinem Buch Agile Softwareentwicklung: Prinzipien, Muster und Praktiken gegeben hat :

  1. High-Level-Module sollten nicht von Low-Level-Modulen abhängen. Beides sollte von Abstraktionen abhängen.
  2. Abstraktionen sollten nicht von Details abhängen. Details sollten von Abstraktionen abhängen.

Es ist also klar, dass es beim DIP im Kern darum geht, die klassische Abhängigkeit zwischen Komponenten auf hoher und niedriger Ebene umzukehren, indem die Interaktion zwischen ihnen abstrahiert wird .

In der traditionellen Softwareentwicklung hängen Komponenten auf hoher Ebene von Komponenten auf niedriger Ebene ab. Daher ist es schwierig, die übergeordneten Komponenten wiederzuverwenden.

3.1. Designauswahl und DIP

Betrachten wir eine einfache StringProcessor- Klasse, die mithilfe einer StringReader- Komponente einen String- Wert abruft und ihn mithilfe einer StringWriter- Komponente an eine andere Stelle schreibt :

public class StringProcessor { private final StringReader stringReader; private final StringWriter stringWriter; public StringProcessor(StringReader stringReader, StringWriter stringWriter) { this.stringReader = stringReader; this.stringWriter = stringWriter; } public void printString() { stringWriter.write(stringReader.getValue()); } } 

Obwohl die Implementierung der StringProcessor- Klasse grundlegend ist, können wir hier verschiedene Entwurfsoptionen auswählen .

Lassen Sie uns jede Designauswahl in separate Elemente aufteilen, um klar zu verstehen, wie sich jedes auf das Gesamtdesign auswirken kann:

  1. StringReader und StringWriter , die Komponenten auf niedriger Ebene, sind konkrete Klassen, die im selben Paket platziert sind. StringProcessor , die übergeordnete Komponente, wird in einem anderen Paket abgelegt. StringProcessor hängt von StringReader und StringWriter ab . Es gibt keine Inversion von Abhängigkeiten, daher kann StringProcessor in einem anderen Kontext nicht wiederverwendet werden.
  2. StringReader und StringWriter sind Schnittstellen, die zusammen mit den Implementierungen im selben Paket platziert werden . StringProcessor hängt jetzt von Abstraktionen ab, die Low-Level-Komponenten jedoch nicht. Wir haben noch keine Umkehrung der Abhängigkeiten erreicht.
  3. StringReader und StringWriter sind Schnittstellen, die zusammen mit StringProcessor im selben Paket platziert sind . Jetzt hat StringProcessor das explizite Eigentum an den Abstraktionen. StringProcessor, StringReader und StringWriter hängen alle von Abstraktionen ab. Wir haben die Inversion der Abhängigkeiten von oben nach unten erreicht, indem wir die Interaktion zwischen den Komponenten abstrahiert haben . StringProcessor kann jetzt in einem anderen Kontext wiederverwendet werden.
  4. StringReader und StringWriter sind Schnittstellen, die in einem von StringProcessor getrennten Paket abgelegt sind . Wir haben die Inversion von Abhängigkeiten erreicht und es ist auch einfacher, StringReader- und StringWriter- Implementierungenzu ersetzen. StringProcessor kann auch in einem anderen Kontext wiederverwendet werden.

Von allen oben genannten Szenarien sind nur die Punkte 3 und 4 gültige Implementierungen des DIP.

3.2. Definieren des Eigentums an den Abstraktionen

Punkt 3 ist eine direkte DIP-Implementierung, bei der die übergeordnete Komponente und die Abstraktion (en) in demselben Paket platziert werden. Daher besitzt die übergeordnete Komponente die Abstraktionen . In dieser Implementierung ist die übergeordnete Komponente für die Definition des abstrakten Protokolls verantwortlich, über das sie mit den untergeordneten Komponenten interagiert.

Ebenso ist Punkt 4 eine entkoppelte DIP-Implementierung. In dieser Variante des Musters besitzen weder die Komponente auf hoher Ebene noch die Komponente auf niedriger Ebene das Eigentum an den Abstraktionen .

Die Abstraktionen werden in einer separaten Ebene platziert, was das Umschalten der Low-Level-Komponenten erleichtert. Gleichzeitig werden alle Komponenten voneinander isoliert, was zu einer stärkeren Einkapselung führt.

3.3. Auswahl der richtigen Abstraktionsebene

In den meisten Fällen sollte die Auswahl der Abstraktionen, die von den Komponenten auf hoher Ebene verwendet werden, recht einfach sein, wobei jedoch eine Einschränkung zu beachten ist: die Abstraktionsebene.

Im obigen Beispiel haben wir DI verwendet, um einen StringReader- Typ in die StringProcessor- Klasse einzufügen . Dies wäre effektiv , solange sich die Abstraktionsebene von StringReader in der Nähe der Domäne von StringProcessor befindet .

Im Gegensatz dazu würden wir nur die eigentlichen Vorteile des DIP vermissen , wenn StringReader beispielsweise ein File- Objekt ist, das einen String- Wert aus einer Datei liest . In diesem Fall wäre die Abstraktionsebene von StringReader viel niedriger als die Ebene der Domäne von StringProcessor .

Einfach ausgedrückt sollte die Abstraktionsebene, mit der die Komponenten auf hoher Ebene mit den Komponenten auf niedriger Ebene zusammenarbeiten, immer in der Nähe der Domäne der ersteren liegen .

4. Java 8-Implementierungen

Wir haben uns bereits eingehend mit den Schlüsselkonzepten des DIP befasst und werden nun einige praktische Implementierungen des Musters in Java 8 untersuchen.

4.1. Direkte DIP-Implementierung

Let's create a demo application that fetches some customers from the persistence layer and processes them in some additional way.

The layer's underlying storage is usually a database, but to keep the code simple, here we'll use a plain Map.

Let's start by defining the high-level component:

public class CustomerService { private final CustomerDao customerDao; // standard constructor / getter public Optional findById(int id) { return customerDao.findById(id); } public List findAll() { return customerDao.findAll(); } }

As we can see, the CustomerService class implements the findById() and findAll() methods, which fetch customers from the persistence layer using a simple DAO implementation. Of course, we could've encapsulated more functionality in the class, but let's keep it like this for simplicity's sake.

In this case, the CustomerDao type is the abstraction that CustomerService uses for consuming the low-level component.

Since this a direct DIP implementation, let's define the abstraction as an interface in the same package of CustomerService:

public interface CustomerDao { Optional findById(int id); List findAll(); } 

By placing the abstraction in the same package of the high-level component, we're making the component responsible for owning the abstraction. This implementation detail is what really inverts the dependency between the high-level component and the low-level one.

In addition, the level of abstraction of CustomerDao is close to the one of CustomerService, which is also required for a good DIP implementation.

Now, let's create the low-level component in a different package. In this case, it's just a basic CustomerDao implementation:

public class SimpleCustomerDao implements CustomerDao { // standard constructor / getter @Override public Optional findById(int id) { return Optional.ofNullable(customers.get(id)); } @Override public List findAll() { return new ArrayList(customers.values()); } }

Finally, let's create a unit test to check the CustomerService class' functionality:

@Before public void setUpCustomerServiceInstance() { var customers = new HashMap(); customers.put(1, new Customer("John")); customers.put(2, new Customer("Susan")); customerService = new CustomerService(new SimpleCustomerDao(customers)); } @Test public void givenCustomerServiceInstance_whenCalledFindById_thenCorrect() { assertThat(customerService.findById(1)).isInstanceOf(Optional.class); } @Test public void givenCustomerServiceInstance_whenCalledFindAll_thenCorrect() { assertThat(customerService.findAll()).isInstanceOf(List.class); } @Test public void givenCustomerServiceInstance_whenCalledFindByIdWithNullCustomer_thenCorrect() { var customers = new HashMap(); customers.put(1, null); customerService = new CustomerService(new SimpleCustomerDao(customers)); Customer customer = customerService.findById(1).orElseGet(() -> new Customer("Non-existing customer")); assertThat(customer.getName()).isEqualTo("Non-existing customer"); }

The unit test exercises the CustomerService API. And, it also shows how to manually inject the abstraction into the high-level component. In most cases, we'd use some kind of DI container or framework to accomplish this.

Additionally, the following diagram shows the structure of our demo application, from a high-level to a low-level package perspective:

4.2. Alternative DIP Implementation

As we discussed before, it's possible to use an alternative DIP implementation, where we place the high-level components, the abstractions, and the low-level ones in different packages.

For obvious reasons, this variant is more flexible, yields better encapsulation of the components, and makes it easier to replace the low-level components.

Of course, implementing this variant of the pattern boils down to just placing CustomerService, MapCustomerDao, and CustomerDao in separate packages.

Therefore, a diagram is sufficient for showing how each component is laid out with this implementation:

5. Java 11 Modular Implementation

It's fairly easy to refactor our demo application into a modular one.

This is a really nice way to demonstrate how the JPMS enforces best programming practices, including strong encapsulation, abstraction, and component reuse through the DIP.

We don't need to reimplement our sample components from scratch. Hence, modularizing our sample application is just a matter of placing each component file in a separate module, along with the corresponding module descriptor.

Here's how the modular project structure will look:

project base directory (could be anything, like dipmodular) |- com.baeldung.dip.services module-info.java   |- com |- baeldung |- dip |- services CustomerService.java |- com.baeldung.dip.daos module-info.java   |- com |- baeldung |- dip |- daos CustomerDao.java |- com.baeldung.dip.daoimplementations module-info.java |- com |- baeldung |- dip |- daoimplementations SimpleCustomerDao.java |- com.baeldung.dip.entities module-info.java |- com |- baeldung |- dip |- entities Customer.java |- com.baeldung.dip.mainapp module-info.java |- com |- baeldung |- dip |- mainapp MainApplication.java 

5.1. The High-Level Component Module

Let's start by placing the CustomerService class in its own module.

We'll create this module in the root directory com.baeldung.dip.services, and add the module descriptor, module-info.java:

module com.baeldung.dip.services { requires com.baeldung.dip.entities; requires com.baeldung.dip.daos; uses com.baeldung.dip.daos.CustomerDao; exports com.baeldung.dip.services; }

For obvious reasons, we won't go into the details on how the JPMS works. Even so, it's clear to see the module dependencies just by looking at the requires directives.

The most relevant detail worth noting here is the uses directive. It states that the module is a client module that consumes an implementation of the CustomerDao interface.

Of course, we still need to place the high-level component, the CustomerService class, in this module. So, within the root directory com.baeldung.dip.services, let's create the following package-like directory structure: com/baeldung/dip/services.

Finally, let's place the CustomerService.java file in that directory.

5.2. The Abstraction Module

Likewise, we need to place the CustomerDao interface in its own module. Therefore, let's create the module in the root directory com.baeldung.dip.daos, and add the module descriptor:

module com.baeldung.dip.daos { requires com.baeldung.dip.entities; exports com.baeldung.dip.daos; }

Now, let's navigate to the com.baeldung.dip.daos directory and create the following directory structure: com/baeldung/dip/daos. Let's place the CustomerDao.java file in that directory.

5.3. The Low-Level Component Module

Logically, we need to put the low-level component, SimpleCustomerDao, in a separate module, too. As expected, the process looks very similar to what we just did with the other modules.

Let's create the new module in the root directory com.baeldung.dip.daoimplementations, and include the module descriptor:

module com.baeldung.dip.daoimplementations { requires com.baeldung.dip.entities; requires com.baeldung.dip.daos; provides com.baeldung.dip.daos.CustomerDao with com.baeldung.dip.daoimplementations.SimpleCustomerDao; exports com.baeldung.dip.daoimplementations; }

In a JPMS context, this is a service provider module, since it declares the provides and with directives.

In this case, the module makes the CustomerDao service available to one or more consumer modules, through the SimpleCustomerDao implementation.

Let's keep in mind that our consumer module, com.baeldung.dip.services, consumes this service through the uses directive.

This clearly shows how simple it is to have a direct DIP implementation with the JPMS, by just defining consumers, service providers, and abstractions in different modules.

Likewise, we need to place the SimpleCustomerDao.java file in this new module. Let's navigate to the com.baeldung.dip.daoimplementations directory, and create a new package-like directory structure with this name: com/baeldung/dip/daoimplementations.

Finally, let's place the SimpleCustomerDao.java file in the directory.

5.4. The Entity Module

Additionally, we have to create another module where we can place the Customer.java class. As we did before, let's create the root directory com.baeldung.dip.entities and include the module descriptor:

module com.baeldung.dip.entities { exports com.baeldung.dip.entities; }

In the package's root directory, let's create the directory com/baeldung/dip/entities and add the following Customer.java file:

public class Customer { private final String name; // standard constructor / getter / toString }

5.5. The Main Application Module

Next, we need to create an additional module that allows us to define our demo application's entry point. Therefore, let's create another root directory com.baeldung.dip.mainapp and place in it the module descriptor:

module com.baeldung.dip.mainapp { requires com.baeldung.dip.entities; requires com.baeldung.dip.daos; requires com.baeldung.dip.daoimplementations; requires com.baeldung.dip.services; exports com.baeldung.dip.mainapp; }

Now, let's navigate to the module's root directory, and create the following directory structure: com/baeldung/dip/mainapp. In that directory, let's add a MainApplication.java file, which simply implements a main() method:

public class MainApplication { public static void main(String args[]) { var customers = new HashMap(); customers.put(1, new Customer("John")); customers.put(2, new Customer("Susan")); CustomerService customerService = new CustomerService(new SimpleCustomerDao(customers)); customerService.findAll().forEach(System.out::println); } }

Lassen Sie uns abschließend die Demo-Anwendung kompilieren und ausführen - entweder in unserer IDE oder über eine Befehlskonsole.

Wie erwartet sollte beim Start der Anwendung eine Liste der Kundenobjekte auf der Konsole ausgedruckt werden:

Customer{name=John} Customer{name=Susan} 

Darüber hinaus zeigt das folgende Diagramm die Abhängigkeiten der einzelnen Module der Anwendung:

6. Fazit

In diesem Tutorial haben wir uns eingehend mit den Schlüsselkonzepten des DIP befasst und verschiedene Implementierungen des Musters in Java 8 und Java 11 gezeigt , wobei letztere das JPMS verwenden.

Alle Beispiele für die Java 8 DIP-Implementierung und die Java 11-Implementierung sind auf GitHub verfügbar.