Afhængighedsinversionsprincippet i Java

1. Oversigt

Dependency Inversion Principle (DIP) er en del af samlingen af ​​objektorienterede programmeringsprincipper, populært kendt som SOLID.

På de bare knogler er DIP et simpelt - men alligevel kraftigt - programmeringsparadigme, som vi kan bruge at implementere velstrukturerede, stærkt afkoblede og genanvendelige softwarekomponenter.

I denne vejledning vi vil undersøge forskellige tilgange til implementering af DIP - en i Java 8 og en i Java 11 ved hjælp af JPMS (Java Platform Module System).

2. Afhængighedsinjektion og inversion af kontrol er ikke DIP-implementeringer

Lad os først og fremmest foretage en grundlæggende skelnen for at få det grundlæggende rigtigt: DIP er hverken afhængighedsinjektion (DI) eller inversion af kontrol (IoC). Alligevel fungerer de alle sammen godt sammen.

Kort sagt handler DI om at fremstille softwarekomponenter til eksplicit at erklære deres afhængighed eller samarbejdspartnere gennem deres API'er i stedet for at erhverve dem selv.

Uden DI er softwarekomponenter tæt koblet til hinanden. Derfor er de svære at genbruge, udskifte, spotte og teste, hvilket resulterer i stive designs.

Med DI overføres ansvaret for at levere komponentafhængighederne og ledningsobjektgraferne fra komponenterne til den underliggende injektionsramme. Fra dette perspektiv er DI bare en måde at opnå IoC på.

På den anden side, IoC er et mønster, hvor styringen af ​​strømmen af ​​en applikation vendes. Med traditionelle programmeringsmetoder styrer vores brugerdefinerede kode strømmen af ​​en applikation. Omvendt med IoC overføres kontrollen til en ekstern ramme eller container.

Rammen er en udvidelig codebase, der definerer hook-point til tilslutning af vores egen kode.

Til gengæld rammer rammen vores kode tilbage gennem en eller flere specialiserede underklasser ved hjælp af grænsefladesimplementeringer og via annoteringer. Forårsrammen er et godt eksempel på denne sidste tilgang.

3. Grundlaget for DIP

For at forstå motivationen bag DIP, lad os starte med dens formelle definition, givet af Robert C. Martin i sin bog, Agil softwareudvikling: principper, mønstre og praksis:

  1. Moduler på højt niveau bør ikke afhænge af moduler på lavt niveau. Begge skal afhænge af abstraktioner.
  2. Abstraktioner bør ikke afhænge af detaljer. Detaljer bør afhænge af abstraktioner.

Så det er klart, at kernen, DIP handler om at invertere den klassiske afhængighed mellem komponenter på højt niveau og lavt niveau ved at fjerne samspillet mellem dem.

I traditionel softwareudvikling er komponenter på højt niveau afhængige af komponenter på lavt niveau. Således er det svært at genbruge komponenterne på højt niveau.

3.1. Designvalg og DIP

Lad os overveje en simpel StringProcessor klasse, der får en Snor værdi ved hjælp af en StringReader komponent og skriver det et andet sted ved hjælp af en StringWriter komponent:

offentlig klasse StringProcessor {private final StringReader stringReader; privat final StringWriter stringWriter; public StringProcessor (StringReader stringReader, StringWriter stringWriter) {this.stringReader = stringReader; this.stringWriter = stringWriter; } offentlig ugyldig printString () {stringWriter.write (stringReader.getValue ()); }} 

Selvom gennemførelsen af StringProcessor klasse er grundlæggende, der er flere designvalg, som vi kan træffe her.

Lad os opdele hvert designvalg i separate emner for at forstå klart, hvordan hver enkelt kan påvirke det overordnede design:

  1. StringReader og StringWriter, komponenterne på lavt niveau, er betonklasser placeret i samme pakke.StringProcessor, er komponenten på højt niveau placeret i en anden pakke. StringProcessor afhænger af StringReader og StringWriter. Der er derfor ingen inversion af afhængigheder StringProcessor kan ikke genbruges i en anden sammenhæng.
  2. StringReader og StringWriter er grænseflader placeret i den samme pakke sammen med implementeringerne. StringProcessor afhænger nu af abstraktioner, men komponenterne på lavt niveau gør det ikke. Vi har endnu ikke opnået inversion af afhængigheder.
  3. StringReader og StringWriter er grænseflader placeret i samme pakke sammen med StringProcessor. Nu, StringProcessor har eksplicit ejerskab af abstraktionerne. StringProcessor, StringLæser, og StringWriter alt afhænger af abstraktioner. Vi har opnået inversion af afhængigheder fra top til bund ved at abstrahere interaktionen mellem komponenterne.StringProcessor kan nu genbruges i en anden sammenhæng.
  4. StringReader og StringWriter er grænseflader placeret i en separat pakke fra StringProcessor. Vi opnåede inversion af afhængigheder, og det er også lettere at erstatte StringReader og StringWriter implementeringer. StringProcessor kan også genbruges i en anden sammenhæng.

Af alle ovenstående scenarier er kun punkt 3 og 4 gyldige implementeringer af DIP.

3.2. Definition af ejerskabet af abstraktionerne

Punkt 3 er en direkte DIP-implementering hvor komponenten på højt niveau og abstraktion (erne) er placeret i den samme pakke. Derfor, komponenten på højt niveau ejer abstraktionerne. I denne implementering er komponenten på højt niveau ansvarlig for at definere den abstrakte protokol, gennem hvilken den interagerer med komponenterne på lavt niveau.

Ligeledes er punkt 4 en mere afkoblet DIP-implementering. I denne variant af mønsteret, hverken komponenten på højt niveau eller lavt niveau har ejerskabet af abstraktionerne.

Abstraktionerne placeres i et separat lag, hvilket letter skift af komponenter på lavt niveau. På samme tid er alle komponenter isoleret fra hinanden, hvilket giver stærkere indkapsling.

3.3. Valg af det rette abstraktionsniveau

I de fleste tilfælde bør valg af abstraktioner, som komponenterne på højt niveau vil bruge, være ret ligetil, men med et forbehold værd at bemærke: abstraktionsniveauet.

I eksemplet ovenfor brugte vi DI til at injicere a StringReader skriv ind i StringProcessor klasse. Dette ville være effektivt så længe abstraktionsniveauet for StringReader er tæt på domænet for StringProcessor.

I modsætning hertil ville vi bare gå glip af DIP's iboende fordele, hvis StringReader er for eksempel en Fil objekt, der læser en Snor værdi fra en fil. I så fald er abstraktionsniveauet for StringReader ville være meget lavere end niveauet for StringProcessor.

For at sige det enkelt, abstraktionsniveauet, som komponenterne på højt niveau vil bruge til at interoperere med de lavt niveau, skal altid være tæt på det tidligere domæne.

4. Java 8-implementeringer

Vi kiggede allerede dybtgående på DIP's nøglekoncepter, så nu undersøger vi et par praktiske implementeringer af mønsteret i Java 8.

4.1. Direkte DIP-implementering

Lad os oprette en demo-applikation, der henter nogle kunder fra persistenslaget og behandler dem på en eller anden måde.

Lagets underliggende lager er normalt en database, men for at holde koden enkel, her bruger vi en almindelig Kort.

Lad os starte med definerer komponenten på højt niveau:

offentlig klasse CustomerService {privat endelig CustomerDao customerDao; // standardkonstruktør / getter offentlig Valgfri findById (int id) {return customerDao.findById (id); } offentlig liste findAll () {returner customerDao.findAll (); }}

Som vi kan se, er Kunde service klasse implementerer findById () og findAll () metoder, der henter kunder fra persistenslaget ved hjælp af en simpel DAO-implementering. Selvfølgelig kunne vi have indkapslet mere funktionalitet i klassen, men lad os holde det sådan for enkelhedens skyld.

I dette tilfælde, det CustomerDao typen er abstraktionen at Kunde service anvendelser til forbrug af lavt niveau.

Da dette er en direkte DIP-implementering, lad os definere abstraktionen som en grænseflade i den samme pakke af Kunde service:

offentlig grænseflade CustomerDao {Valgfri findById (int id); Liste findAll (); } 

Ved at placere abstraktionen i den samme pakke som komponenten på højt niveau, gør vi komponenten ansvarlig for at eje abstraktionen. Denne implementeringsdetalje er hvad der inverterer afhængigheden mellem komponenten på højt niveau og den lavt niveau.

Ud over, niveauet for abstraktion af CustomerDao er tæt på den ene af Kunde service, hvilket også kræves for en god DIP-implementering.

Lad os nu oprette komponenten på lavt niveau i en anden pakke. I dette tilfælde er det bare et grundlæggende CustomerDao implementering:

offentlig klasse SimpleCustomerDao implementerer CustomerDao {// standardkonstruktør / getter @Override offentlig Valgfri findById (int id) {return Optional.ofNullable (customers.get (id)); } @ Override public List findAll () {returner ny ArrayList (customers.values ​​()); }}

Lad os endelig oprette en enhedstest for at kontrollere Kunde service klasse 'funktionalitet:

@Før offentlig ugyldigt setUpCustomerServiceInstance () {var kunder = nyt HashMap (); customers.put (1, ny kunde ("John")); customers.put (2, ny kunde ("Susan")); customerService = ny CustomerService (ny SimpleCustomerDao (kunder)); } @Test offentlig ugyldighed givenCustomerServiceInstance_whenCalledFindById_thenCorrect () {assertThat (customerService.findById (1)). IsInstanceOf (Optional.class); } @Test offentlig ugyldighed givenCustomerServiceInstance_whenCalledFindAll_thenCorrect () {assertThat (customerService.findAll ()). IsInstanceOf (List.class); } @Test offentlig ugyldighed givenCustomerServiceInstance_whenCalledFindByIdWithNullCustomer_thenCorrect () {var kunder = ny HashMap (); customers.put (1, null); customerService = ny CustomerService (ny SimpleCustomerDao (kunder)); Kundekunde = customerService.findById (1) .orElseGet (() -> ny kunde ("Ikke-eksisterende kunde")); assertThat (customer.getName ()). isEqualTo ("Ikke-eksisterende kunde"); }

Enhedstesten udøver Kunde service API. Og det viser også, hvordan man manuelt injicerer abstraktionen i komponenten på højt niveau. I de fleste tilfælde vil vi bruge en slags DI-container eller ramme til at opnå dette.

Derudover viser følgende diagram strukturen i vores demo-applikation fra et højt niveau til et lavt niveau pakkeperspektiv:

4.2. Alternativ DIP-implementering

Som vi diskuterede før, er det muligt at bruge en alternativ DIP-implementering, hvor vi placerer komponenterne på højt niveau, abstraktionerne og de lave niveauer i forskellige pakker.

Af åbenlyse grunde er denne variant mere fleksibel, giver bedre indkapsling af komponenterne og gør det lettere at udskifte komponenter på lavt niveau.

Selvfølgelig implementerer denne variant af mønsteret kun at placere Kunde service, KortKundeDao, og CustomerDao i separate pakker.

Derfor er et diagram tilstrækkeligt til at vise, hvordan hver komponent er lagt ud med denne implementering:

5. Java 11 modulær implementering

Det er ret nemt at omformulere vores demo-applikation til en modulær.

Dette er en rigtig god måde at demonstrere, hvordan JPMS håndhæver bedste programmeringspraksis, herunder stærk indkapsling, abstraktion og genbrug af komponenter gennem DIP.

Vi behøver ikke at genimplementere vores prøvekomponenter fra bunden. Derfor, modulering af vores prøveapplikation er bare et spørgsmål om at placere hver komponentfil i et separat modul sammen med den tilsvarende modulbeskrivelse.

Sådan ser den modulære projektstruktur ud:

projektbaseret katalog (kunne være hvad som helst, som 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. Komponentmodulet på højt niveau

Lad os starte med at placere Kunde service klasse i sit eget modul.

Vi opretter dette modul i rodmappen com.baeldung.dip.services, og tilføj modulbeskrivelsen, modul-info.java:

modul com.baeldung.dip.services {kræver com.baeldung.dip.entities; kræver com.baeldung.dip.daos; bruger com.baeldung.dip.daos.CustomerDao; eksport com.baeldung.dip.services; }

Af åbenlyse grunde går vi ikke nærmere ind på, hvordan JPMS fungerer. Alligevel er det klart at se modulafhængighederne bare ved at se på kræver direktiver.

Den mest relevante detalje, der er værd at bemærke her, er anvendelser direktiv. Det hedder det modulet er et klientmodul der forbruger en implementering af CustomerDao interface.

Selvfølgelig skal vi stadig placere komponenten på højt niveau, den Kunde service klasse i dette modul. Så inden for rodmappen com.baeldung.dip.tjenester, lad os oprette følgende pakke-lignende bibliotekstruktur: com / baeldung / dip / services.

Lad os endelig placere Kundeservice.java fil i den mappe.

5.2. Abstraktionsmodulet

Ligeledes er vi nødt til at placere CustomerDao interface i sit eget modul. Lad os derfor oprette modulet i rodmappen com.baeldung.dip.daos, og tilføj modulbeskrivelsen:

modul com.baeldung.dip.daos {kræver com.baeldung.dip.entities; eksport com.baeldung.dip.daos; }

Lad os nu navigere til com.baeldung.dip.daos mappe og opret følgende katalogstruktur: com / baeldung / dip / daos. Lad os placere CustomerDao.java fil i den mappe.

5.3. Komponentmodulet på lavt niveau

Logisk set er vi nødt til at sætte komponenten på lavt niveau, SimpleCustomerDao, også i et separat modul. Som forventet ser processen meget ud som hvad vi lige gjorde med de andre moduler.

Lad os oprette det nye modul i rodmappen com.baeldung.dip.daoimplementations, og inkluderer modulbeskrivelsen:

modul com.baeldung.dip.daoimplementations {kræver com.baeldung.dip.entities; kræver com.baeldung.dip.daos; giver com.baeldung.dip.daos.CustomerDao com.baeldung.dip.daoimplementations.SimpleCustomerDao; eksport com.baeldung.dip.daoimplementations; }

I JPMS-sammenhæng dette er et tjenesteudbydermodul, da det erklærer giver og med direktiver.

I dette tilfælde fremstiller modulet CustomerDao tjeneste til rådighed for et eller flere forbrugermoduler via SimpleCustomerDao implementering.

Lad os huske på, at vores forbrugermodul, com.baeldung.dip.services, bruger denne tjeneste gennem anvendelser direktiv.

Dette viser tydeligt hvor simpelt det er at have en direkte DIP-implementering med JPMS ved blot at definere forbrugere, tjenesteudbydere og abstraktioner i forskellige moduler.

Ligeledes er vi nødt til at placere SimpleCustomerDao.java fil i dette nye modul. Lad os navigere til com.baeldung.dip.daoimplementations katalog, og opret en ny pakke-lignende bibliotekstruktur med dette navn: com / baeldung / dip / daoimplementations.

Lad os endelig placere SimpleCustomerDao.java fil i biblioteket.

5.4. Enhedsmodulet

Derudover skal vi oprette et andet modul, hvor vi kan placere Kunde.java klasse. Som vi gjorde før, lad os oprette rodmappen com.baeldung.dip.entities og inkluderer modulbeskrivelsen:

modul com.baeldung.dip.entities {eksporterer com.baeldung.dip.entities; }

Lad os oprette biblioteket i pakkens rodmappe com / baeldung / dip / entities og tilføj følgende Kunde.java fil:

offentlig klasse kunde {privat endelig strengnavn; // standard konstruktør / getter / toString}

5.5. Hovedapplikationsmodulet

Dernæst skal vi oprette et ekstra modul, der giver os mulighed for at definere vores demo-applikations indgangssted. Lad os derfor oprette en anden rodkatalog com.baeldung.dip.mainapp og placer modulbeskrivelsen i den:

modul com.baeldung.dip.mainapp {kræver com.baeldung.dip.entities; kræver com.baeldung.dip.daos; kræver com.baeldung.dip.daoimplementations; kræver com.baeldung.dip.services; eksport com.baeldung.dip.mainapp; }

Lad os nu navigere til modulets rodkatalog og oprette følgende katalogstruktur: com / baeldung / dip / mainapp. Lad os tilføje en i den mappe MainApplication.java fil, som simpelthen implementerer en hoved () metode:

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

Lad os endelig kompilere og køre demo-applikationen - enten inden for vores IDE eller fra en kommandokonsol.

Som forventet skal vi se en liste over Kunde objekter, der udskrives til konsollen, når applikationen starter:

Kunde {name = John} Kunde {name = Susan} 

Derudover viser følgende diagram afhængighederne for hvert modul i applikationen:

6. Konklusion

I denne vejledning vi dykkede dybt ned i DIP's nøglekoncepter, og vi viste også forskellige implementeringer af mønsteret i Java 8 og Java 11, hvor sidstnævnte bruger JPMS.

Alle eksemplerne til implementering af Java 8 DIP og implementering af Java 11 er tilgængelige på GitHub.