Het afhankelijkheidsinversieprincipe in Java

1. Overzicht

Het Dependency Inversion Principle (DIP) maakt deel uit van de verzameling objectgeoriënteerde programmeerprincipes die in de volksmond bekend staan ​​als SOLID.

In wezen is de DIP een eenvoudig maar krachtig programmeerparadigma dat we kunnen gebruiken om goed gestructureerde, sterk ontkoppelde en herbruikbare softwarecomponenten te implementeren.

In deze tutorial we zullen verschillende benaderingen onderzoeken voor het implementeren van de DIP - een in Java 8 en een in Java 11 met behulp van de JPMS (Java Platform Module System).

2. Afhankelijkheidsinjectie en inversie van controle zijn geen DIP-implementaties

Laten we eerst en vooral een fundamenteel onderscheid maken om de basis goed te krijgen: de DIP is noch afhankelijkheidsinjectie (DI), noch inversie van controle (IoC). Toch werken ze allemaal geweldig samen.

Simpel gezegd, DI gaat over het maken van softwarecomponenten die hun afhankelijkheden of medewerkers expliciet aangeven via hun API's, in plaats van ze zelf te verwerven.

Zonder DI zijn softwarecomponenten nauw met elkaar verbonden. Daarom zijn ze moeilijk te hergebruiken, vervangen, bespotten en testen, wat resulteert in rigide ontwerpen.

Met DI wordt de verantwoordelijkheid voor het leveren van de componentafhankelijkheden en bedradingsobjectgrafieken overgedragen van de componenten naar het onderliggende injectiekader. Vanuit dat perspectief is DI slechts een manier om IoC te bereiken.

Aan de andere kant, IoC is een patroon waarin de controle over de stroom van een applicatie wordt omgekeerd. Met traditionele programmeermethodologieën heeft onze aangepaste code de controle over de stroom van een applicatie. Omgekeerd, met IoC wordt de controle overgebracht naar een extern framework of container.

Het framework is een uitbreidbare codebase, die hookpoints definieert om onze eigen code in te pluggen.

Het framework roept op zijn beurt onze code terug via een of meer gespecialiseerde subklassen, met behulp van interfaces 'implementaties en via annotaties. Het Spring framework is een mooi voorbeeld van deze laatste benadering.

3. Grondbeginselen van DIP

Om de motivatie achter de DIP te begrijpen, laten we beginnen met de formele definitie, gegeven door Robert C.Martin in zijn boek: Agile softwareontwikkeling: principes, patronen en praktijken:

  1. Modules op hoog niveau mogen niet afhankelijk zijn van modules op laag niveau. Beide zouden afhankelijk moeten zijn van abstracties.
  2. Abstracties zouden niet afhankelijk moeten zijn van details. Details moeten afhangen van abstracties.

Het is dus duidelijk dat in de kern, de DIP gaat over het omkeren van de klassieke afhankelijkheid tussen componenten op hoog en laag niveau door de interactie daartussen weg te abstraheren.

Bij traditionele softwareontwikkeling zijn componenten op hoog niveau afhankelijk van componenten op laag niveau. Het is dus moeilijk om de componenten op hoog niveau opnieuw te gebruiken.

3.1. Ontwerpkeuzes en de DIP

Laten we eens kijken naar een simpele StringProcessor klasse die een Draad waarde met behulp van een StringReader component, en schrijft het ergens anders met een StringWriter component:

openbare klasse StringProcessor {privé laatste StringReader stringReader; private laatste StringWriter stringWriter; openbare StringProcessor (StringReader stringReader, StringWriter stringWriter) {this.stringReader = stringReader; this.stringWriter = stringWriter; } openbare ongeldige printString () {stringWriter.write (stringReader.getValue ()); }} 

Hoewel de implementatie van de StringProcessor klasse is basic, er zijn verschillende ontwerpkeuzes die we hier kunnen maken.

Laten we elke ontwerpkeuze opsplitsen in afzonderlijke items, om duidelijk te begrijpen hoe elk het algehele ontwerp kan beïnvloeden:

  1. StringReader en StringWriter, de low-level componenten, zijn betonklassen die in hetzelfde pakket zijn geplaatst.StringProcessor, wordt de component op hoog niveau in een andere verpakking geplaatst. StringProcessor hangt af van StringReader en StringWriter. Er is dus geen inversie van afhankelijkheden StringProcessor is niet herbruikbaar in een andere context.
  2. StringReader en StringWriter zijn interfaces die samen met de implementaties in hetzelfde pakket zijn geplaatst. StringProcessor hangt nu af van abstracties, maar de componenten op laag niveau niet. We hebben nog geen inversie van afhankelijkheden bereikt.
  3. StringReader en StringWriter zijn interfaces die samen met StringProcessor. Nu, StringProcessor heeft het expliciete eigendom van de abstracties. StringProcessor, StringReader, en StringWriter ze zijn allemaal afhankelijk van abstracties. We hebben de afhankelijkheden van boven naar beneden omgekeerd door de interactie tussen de componenten te abstraheren.StringProcessor is nu herbruikbaar in een andere context.
  4. StringReader en StringWriter zijn interfaces die in een apart pakket zijn geplaatst van StringProcessor. We hebben inversie van afhankelijkheden bereikt en het is ook gemakkelijker te vervangen StringReader en StringWriter implementaties. StringProcessor is ook herbruikbaar in een andere context.

Van alle bovenstaande scenario's zijn alleen items 3 en 4 geldige implementaties van de DIP.

3.2. Het eigendom van de abstracties definiëren

Item 3 is een directe DIP-implementatie, waarbij de high-level component en de abstractie (s) in hetzelfde pakket worden geplaatst. Vandaar, de component op hoog niveau is eigenaar van de abstracties. In deze implementatie is de component op hoog niveau verantwoordelijk voor het definiëren van het abstracte protocol waarmee het samenwerkt met de componenten op laag niveau.

Evenzo is item 4 een meer ontkoppelde DIP-implementatie. In deze variant van het patroon, noch de high-level component, noch de low-level component hebben de eigendom van de abstracties.

De abstracties worden in een aparte laag geplaatst, wat het schakelen tussen de low-level componenten vergemakkelijkt. Tegelijkertijd worden alle componenten van elkaar geïsoleerd, wat een sterkere inkapseling oplevert.

3.3. Het juiste abstractieniveau kiezen

In de meeste gevallen zou het kiezen van de abstracties die de componenten op hoog niveau zullen gebruiken redelijk eenvoudig moeten zijn, maar met één voorbehoud: het abstractieniveau.

In het bovenstaande voorbeeld hebben we DI gebruikt om een StringReader typ in het StringProcessor klasse. Dit zou effectief zijn zolang het abstractieniveau van StringReader ligt dicht bij het domein van StringProcessor.

Daarentegen zouden we de intrinsieke voordelen van de DIP missen als StringReader is bijvoorbeeld een het dossier object dat een Draad waarde uit een bestand. In dat geval is het abstractieniveau van StringReader zou veel lager zijn dan het niveau van het domein van StringProcessor.

Simpel gezegd, het abstractieniveau dat de componenten op hoog niveau zullen gebruiken om samen te werken met de componenten op laag niveau, moet altijd dicht bij het domein van de eerste liggen.

4. Java 8-implementaties

We hebben al diepgaand gekeken naar de belangrijkste concepten van de DIP, dus nu gaan we enkele praktische implementaties van het patroon in Java 8 onderzoeken.

4.1. Directe DIP-implementatie

Laten we een demo-applicatie maken die enkele klanten uit de persistentielaag haalt en ze op een andere manier verwerkt.

De onderliggende opslag van de laag is meestal een database, maar om de code eenvoudig te houden, gebruiken we hier een plain Kaart.

Laten we beginnen het definiëren van de component op hoog niveau:

openbare klasse CustomerService {privé eindklant - klantdomein; // standard constructor / getter public Optioneel findById (int id) {return customerensional.findById (id); } openbare lijst findAll () {return customer Dao.findAll (); }}

Zoals we kunnen zien, is de Klantenservice class implementeert het findById () en vind alle() methoden, die klanten uit de persistentielaag halen met behulp van een eenvoudige DAO-implementatie. We hadden natuurlijk meer functionaliteit in de klas kunnen opnemen, maar laten we het voor de eenvoud zo houden.

In dit geval, de Klantdelta type is de abstractie dat Klantenservice gebruikt voor het consumeren van de low-level component.

Aangezien dit een directe DIP-implementatie is, gaan we de abstractie definiëren als een interface in hetzelfde pakket van Klantenservice:

openbare interface Customer Dao {Optioneel findById (int id); Lijst findAll (); } 

Door de abstractie in hetzelfde pakket van de high-level component te plaatsen, maken we de component verantwoordelijk voor het bezitten van de abstractie. Dit implementatiedetail is wat echt de afhankelijkheid tussen de component op hoog niveau en de component op laag niveau omkeert.

Daarnaast, het abstractieniveau van Klantdelta ligt dicht bij die van Klantenservice, wat ook nodig is voor een goede DIP-implementatie.

Laten we nu de low-level component in een ander pakket maken. In dit geval is het slechts een basis Klantdelta implementatie:

public class SimpleCustomerbao implementeert Customer Dao {// standaard constructor / getter @Override public Optioneel findById (int id) {return Optional.ofNullable (customers.get (id)); } @Override openbare lijst findAll () {retourneer nieuwe ArrayList (customers.values ​​()); }}

Laten we tot slot een unit-test maken om het Klantenservice class 'functionaliteit:

@Before public void setUpCustomerServiceInstance () {var klanten = nieuwe HashMap (); customers.put (1, nieuwe klant ("Jan")); customers.put (2, nieuwe klant ("Susan")); customerService = nieuwe CustomerService (nieuwe SimpleCustomerensional (klanten)); } @Test openbare leegte gegevenCustomerServiceInstance_whenCalledFindById_thenCorrect () {assertThat (customerService.findById (1)). IsInstanceOf (Optioneel.klasse); } @Test openbare leegte gegevenCustomerServiceInstance_whenCalledFindAll_thenCorrect () {assertThat (customerService.findAll ()). IsInstanceOf (List.class); } @Test openbare leegte gegevenCustomerServiceInstance_whenCalledFindByIdWithNullCustomer_thenCorrect () {var klanten = nieuwe HashMap (); klanten.put (1, null); customerService = nieuwe CustomerService (nieuwe SimpleCustomerensional (klanten)); Klant klant = customerService.findById (1) .orElseGet (() -> nieuwe klant ("niet-bestaande klant")); assertThat (customer.getName ()). isEqualTo ("Niet-bestaande klant"); }

De unit-test oefent het Klantenservice API. En het laat ook zien hoe de abstractie handmatig in de component op hoog niveau kan worden geïnjecteerd. In de meeste gevallen gebruiken we een soort DI-container of framework om dit te bereiken.

Bovendien toont het volgende diagram de structuur van onze demo-applicatie, van een hoog niveau tot een laag niveau pakketperspectief:

4.2. Alternatieve DIP-implementatie

Zoals we eerder hebben besproken, is het mogelijk om een ​​alternatieve DIP-implementatie te gebruiken, waarbij we de componenten op hoog niveau, de abstracties en de componenten op laag niveau in verschillende pakketten plaatsen.

Deze variant is om voor de hand liggende redenen flexibeler, levert een betere inkapseling van de componenten op en maakt het gemakkelijker om de low-level componenten te vervangen.

Het implementeren van deze variant van het patroon komt natuurlijk neer op plaatsen Klantenservice, Kaart Klantdelta, en Klantdelta in afzonderlijke pakketten.

Daarom is een diagram voldoende om te laten zien hoe elk onderdeel is ingedeeld met deze implementatie:

5. Java 11 modulaire implementatie

Het is vrij eenvoudig om onze demo-applicatie om te bouwen tot een modulaire applicatie.

Dit is echt een leuke manier om te demonstreren hoe de JPMS de beste programmeerpraktijken afdwingt, inclusief sterke inkapseling, abstractie en hergebruik van componenten via de DIP.

We hoeven onze voorbeeldcomponenten niet helemaal opnieuw te implementeren. Vandaar, Het modulariseren van onze voorbeeldtoepassing is gewoon een kwestie van elk componentbestand in een aparte module plaatsen, samen met de bijbehorende modulebeschrijving.

Hier is hoe de modulaire projectstructuur eruit zal zien:

project basismap (kan van alles zijn, zoals dipmodular) | - com.baeldung.dip.services module-info.java | - com | - baeldung | - dip | - services CustomerService.java | - module com.baeldung.dip.daos -info.java | - com | - baeldung | - dip | - daos Customerensional.java | - com.baeldung.dip.daoimplementations module-info.java | - com | - baeldung | - dip | - daoimplementations SimpleCustomerensional.java | - com.baeldung.dip.entities module-info.java | - com | - baeldung | - dip | - entiteiten Customer.java | - com.baeldung.dip.mainapp module-info.java | - com | - baeldung | - dip | - mainapp MainApplication.java 

5.1. De componentenmodule op hoog niveau

Laten we beginnen met het plaatsen van de Klantenservice klasse in zijn eigen module.

We maken deze module in de root-directory com.baeldung.dip.services, en voeg de module descriptor toe, module-info.java:

module com.baeldung.dip.services {vereist com.baeldung.dip.entities; vereist com.baeldung.dip.daos; gebruikt com.baeldung.dip.daos.Customerdred; exporteert com.baeldung.dip.services; }

Om voor de hand liggende redenen zullen we niet ingaan op de details over hoe de JPMS werkt. Toch is het duidelijk om de module-afhankelijkheden te zien door gewoon naar het vereist richtlijnen.

Het meest relevante detail dat het vermelden waard is, is het toepassingen richtlijn. Het zegt dat de module is een cliëntmodule dat verbruikt een implementatie van de Klantdelta koppel.

Natuurlijk moeten we nog steeds de component op hoog niveau plaatsen, de Klantenservice class, in deze module. Dus binnen de hoofdmap com.baeldung.dip.services, laten we de volgende pakketachtige directorystructuur maken: com / baeldung / dip / services.

Laten we tot slot het Klantenservice.java bestand in die map.

5.2. De abstractiemodule

Evenzo moeten we de Klantdelta interface in zijn eigen module. Laten we daarom de module in de hoofdmap maken com.baeldung.dip.daos, en voeg de modulebeschrijving toe:

module com.baeldung.dip.daos {vereist com.baeldung.dip.entities; exporteert com.baeldung.dip.daos; }

Laten we nu naar het com.baeldung.dip.daos directory en creëer de volgende directorystructuur: com / baeldung / dip / daos. Laten we de Customerbao.java bestand in die map.

5.3. De Low Level Component Module

Logischerwijs moeten we de low-level component plaatsen, SimpleCustomerensional, ook in een aparte module. Zoals verwacht lijkt het proces erg op wat we net hebben gedaan met de andere modules.

Laten we de nieuwe module in de hoofdmap maken com.baeldung.dip.dao-implementaties, en voeg de modulebeschrijving toe:

module com.baeldung.dip.daoimplementations {vereist com.baeldung.dip.entities; vereist com.baeldung.dip.daos; biedt com.baeldung.dip.daos.Customerbao met com.baeldung.dip.daoimplementations.SimpleCustomerbao; exporteert com.baeldung.dip.daoimplementations; }

In een JPMS-context, dit is een serviceprovider-module, aangezien het de biedt en met richtlijnen.

In dit geval maakt de module de Klantdelta service beschikbaar voor een of meer consumentenmodules, via de SimpleCustomerensional implementatie.

Laten we in gedachten houden dat onze consumentenmodule, com.baeldung.dip.services, verbruikt deze service via de toepassingen richtlijn.

Dit is duidelijk te zien hoe eenvoudig het is om een ​​directe DIP-implementatie met het JPMS te hebben, door alleen consumenten, serviceproviders en abstracties in verschillende modules te definiëren.

Evenzo moeten we de SimpleCustomerbao.java bestand in deze nieuwe module. Laten we naar het com.baeldung.dip.dao-implementaties directory en maak een nieuwe pakketachtige directorystructuur met deze naam: com / baeldung / dip / daoimplementations.

Laten we tot slot het SimpleCustomerbao.java bestand in de directory.

5.4. De entiteitsmodule

Bovendien moeten we nog een module maken waar we het Klant.java klasse. Laten we, zoals we eerder hebben gedaan, de hoofdmap maken com.baeldung.dip.entities en voeg de modulebeschrijving toe:

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

Laten we in de hoofdmap van het pakket de map maken com / baeldung / dip / entiteiten en voeg het volgende toe Klant.java het dossier:

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

5.5. De belangrijkste toepassingsmodule

Vervolgens moeten we een extra module maken waarmee we het startpunt van onze demo-applicatie kunnen definiëren. Laten we daarom nog een root-directory maken com.baeldung.dip.mainapp en plaats daarin de module descriptor:

module com.baeldung.dip.mainapp {vereist com.baeldung.dip.entities; vereist com.baeldung.dip.daos; vereist com.baeldung.dip.daoimplementations; vereist com.baeldung.dip.services; exporteert com.baeldung.dip.mainapp; }

Laten we nu naar de hoofdmap van de module navigeren en de volgende mapstructuur maken: com / baeldung / dip / mainapp. Laten we in die map een MainApplication.java -bestand, dat eenvoudig een hoofd() methode:

openbare klasse MainApplication {openbare statische leegte hoofd (String args []) {var klanten = nieuwe HashMap (); customers.put (1, nieuwe klant ("Jan")); customers.put (2, nieuwe klant ("Susan")); CustomerService customerService = nieuwe CustomerService (nieuwe SimpleCustomerensional (klanten)); customerService.findAll (). forEach (System.out :: println); }}

Laten we tot slot de demo-applicatie compileren en uitvoeren - vanuit onze IDE of vanuit een commandoconsole.

Zoals verwacht zouden we een lijst met Klant objecten die naar de console worden afgedrukt wanneer de toepassing opstart:

Klant {name = John} Klant {name = Susan} 

Bovendien toont het volgende diagram de afhankelijkheden van elke module van de applicatie:

6. Conclusie

In deze tutorial we hebben een diepe duik genomen in de belangrijkste concepten van de DIP, en we hebben ook verschillende implementaties van het patroon in Java 8 en Java 11 laten zien, waarbij de laatste de JPMS gebruikt.

Alle voorbeelden voor de Java 8 DIP-implementatie en de Java 11-implementatie zijn beschikbaar op GitHub.


$config[zx-auto] not found$config[zx-overlay] not found