Prinsippet om avhengighetsinversjon i Java

1. Oversikt

Dependency Inversion Principle (DIP) er en del av samlingen av objektorienterte programmeringsprinsipper, kjent som SOLID.

På bare bein er DIP et enkelt - men likevel kraftig - programmeringsparadigme som vi kan bruke å implementere velstrukturerte, sterkt frakoblet og gjenbrukbare programvarekomponenter.

I denne veiledningen, vi vil utforske forskjellige tilnærminger for implementering av DIP - en i Java 8 og en i Java 11 ved hjelp av JPMS (Java Platform Module System).

2. Avhengighetsinjeksjon og inversjon av kontroll er ikke DIP-implementeringer

La oss først og fremst gjøre et grunnleggende skille for å få det grunnleggende til rette: DIP er verken avhengighetsinjeksjon (DI) eller inversjon av kontroll (IoC). Likevel jobber de alle sammen bra.

Enkelt sagt handler DI om å lage programvarekomponenter som eksplisitt erklærer deres avhengighet eller samarbeidspartnere gjennom sine API-er, i stedet for å skaffe dem selv.

Uten DI er programvarekomponenter tett koblet til hverandre. Derfor er de vanskelige å gjenbruke, erstatte, spotte og teste, noe som resulterer i stive design.

Med DI overføres ansvaret for å levere komponentavhengighetene og grafene for ledningsobjekter fra komponentene til det underliggende injeksjonsrammeverket. Fra det perspektivet er DI bare en måte å oppnå IoC på.

På den andre siden, IoC er et mønster der kontrollen av strømmen til en applikasjon blir reversert. Med tradisjonelle programmeringsmetoder har vår tilpassede kode kontroll over flyten til en applikasjon. Omvendt, med IoC overføres kontrollen til et eksternt rammeverk eller container.

Rammeverket er en utvidbar kodebase, som definerer krokpunkter for å plugge inn vår egen kode.

I sin tur kaller rammeverket tilbake koden vår gjennom en eller flere spesialiserte underklasser, ved hjelp av grensesnittens implementeringer og via merknader. Vårrammeverket er et fint eksempel på denne siste tilnærmingen.

3. Grunnleggende om DIP

For å forstå motivasjonen bak DIP, la oss starte med den formelle definisjonen, gitt av Robert C. Martin i sin bok, Agil programvareutvikling: prinsipper, mønstre og praksis:

  1. Moduler på høyt nivå bør ikke avhenge av moduler på lavt nivå. Begge skal avhenge av abstraksjoner.
  2. Abstraksjoner bør ikke avhenge av detaljer. Detaljer bør avhenge av abstraksjoner.

Så det er klart at kjernen, DIP handler om å invertere den klassiske avhengigheten mellom komponenter på høyt nivå og lavt nivå ved å trekke bort samspillet mellom dem.

I tradisjonell programvareutvikling er komponenter på høyt nivå avhengige av komponenter på lavt nivå. Dermed er det vanskelig å gjenbruke komponentene på høyt nivå.

3.1. Designvalg og DIP

La oss vurdere en enkel StringProsessor klasse som får en String verdi ved hjelp av en StringReader komponent, og skriver den et annet sted ved hjelp av en StringWriter komponent:

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

Selv om implementeringen av StringProsessor klasse er grunnleggende, det er flere designvalg vi kan ta her.

La oss dele hvert designvalg opp i separate elementer for å forstå tydelig hvordan hver enkelt kan påvirke det generelle designet:

  1. StringReader og StringWriter, komponentene på lavt nivå, er betongklasser plassert i samme pakke.StringProsessor, er høynivåkomponenten plassert i en annen pakke. StringProsessor kommer an på StringReader og StringWriter. Det er ingen inversjon av avhengigheter, derfor StringProsessor kan ikke gjenbrukes i en annen sammenheng.
  2. StringReader og StringWriter er grensesnitt plassert i samme pakke sammen med implementeringene. StringProsessor avhenger nå av abstraksjoner, men komponenter på lavt nivå gjør det ikke. Vi har ikke oppnådd inversjon av avhengigheter ennå.
  3. StringReader og StringWriter er grensesnitt plassert i samme pakke sammen med StringProsessor. Nå, StringProsessor har eksplisitt eierskap til abstraksjonene. StringProsessor, Strengleser, og StringWriter alt avhenger av abstraksjoner. Vi har oppnådd inversjon av avhengigheter fra topp til bunn ved å abstrakte samspillet mellom komponentene.StringProsessor er nå gjenbrukbar i en annen sammenheng.
  4. StringReader og StringWriter er grensesnitt plassert i en egen pakke fra StringProsessor. Vi oppnådde inversjon av avhengigheter, og det er også lettere å erstatte StringReader og StringWriter implementeringer. StringProsessor er også gjenbrukbar i en annen sammenheng.

Av alle ovennevnte scenarier er bare punkt 3 og 4 gyldige implementeringer av DIP.

3.2. Definere eierforholdet til abstraksjonene

Punkt 3 er en direkte DIP-implementering, der komponenten på høyt nivå og abstraksjonen (e) er plassert i samme pakke. Derfor, komponenten på høyt nivå eier abstraksjonene. I denne implementeringen er komponenten på høyt nivå ansvarlig for å definere den abstrakte protokollen som den interagerer med komponentene på lavt nivå.

På samme måte er post 4 en mer frakoblet DIP-implementering. I denne varianten av mønsteret, verken høykomponent eller lavnivåkomponenter har eierskapet til abstraksjonene.

Abstraksjonene er plassert i et eget lag, som gjør det lettere å bytte komponenter på lavt nivå. Samtidig er alle komponentene isolert fra hverandre, noe som gir sterkere innkapsling.

3.3. Velge riktig abstraksjonsnivå

I de fleste tilfeller bør det være ganske greit å velge abstraksjoner som komponentene på høyt nivå vil bruke, men med en advarsel som er verdt å merke seg: abstraksjonsnivået.

I eksemplet ovenfor brukte vi DI til å injisere a StringReader skriv inn i StringProsessor klasse. Dette ville være effektivt så lenge abstraksjonsnivået til StringReader er nær domenet til StringProsessor.

I motsetning til dette, vil vi bare savne DIPs iboende fordeler hvis StringReader er for eksempel en Fil objekt som leser en String verdi fra en fil. I så fall nivået på abstraksjon av StringReader ville være mye lavere enn nivået på domenet til StringProsessor.

For å si det enkelt, abstraksjonsnivået som komponentene på høyt nivå vil bruke til å samarbeide med lavnivåene, bør alltid være nær det tidligere domenet.

4. Java 8-implementeringer

Vi så allerede i dybden på DIPs nøkkelkonsepter, så nå skal vi utforske noen få praktiske implementeringer av mønsteret i Java 8.

4.1. Direkte DIP-implementering

La oss lage en demo-applikasjon som henter noen kunder fra utholdenhetslaget og behandler dem på en eller annen måte.

Lagets underliggende lagring er vanligvis en database, men for å holde koden enkel, her bruker vi en vanlig Kart.

La oss starte med definere komponenten på høyt nivå:

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

Som vi kan se, er Kundeservice klasse implementerer findById () og findAll () metoder, som henter kunder fra utholdenhetslaget ved hjelp av en enkel DAO-implementering. Selvfølgelig kunne vi ha innkapslet mer funksjonalitet i klassen, men la oss beholde det slik for enkelhets skyld.

I dette tilfellet, de CustomerDao typen er abstraksjonen at Kundeservice bruksområder for forbruk av lavnivåkomponenten

Siden dette er en direkte DIP-implementering, la oss definere abstraksjonen som et grensesnitt i samme pakke med Kundeservice:

offentlig grensesnitt CustomerDao {Valgfri findById (int id); Liste findAll (); } 

Ved å plassere abstraksjonen i den samme pakken som komponenten på høyt nivå, gjør vi komponenten ansvarlig for å eie abstraksjonen. Denne implementeringsdetaljen er hva som inverterer avhengigheten mellom komponenten på høyt nivå og den lave.

I tillegg, nivået av abstraksjon av CustomerDao er nær den ene av Kundeservice, som også kreves for en god DIP-implementering.

La oss nå lage lavnivåkomponenten i en annen pakke. I dette tilfellet er det bare et grunnleggende CustomerDao gjennomføring:

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

Til slutt, la oss lage en enhetstest for å sjekke Kundeservice klassens funksjonalitet:

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

Enhetstesten utøver Kundeservice API. Og det viser også hvordan man manuelt injiserer abstraksjonen i komponenten på høyt nivå. I de fleste tilfeller vil vi bruke en slags DI-container eller rammeverk for å oppnå dette.

I tillegg viser følgende diagram strukturen til demo-applikasjonen vår, fra et høyt nivå til et lavt nivå pakkeperspektiv:

4.2. Alternativ DIP-implementering

Som vi diskuterte tidligere, er det mulig å bruke en alternativ DIP-implementering, der vi plasserer komponenter på høyt nivå, abstraksjoner og lave nivåer i forskjellige pakker.

Av åpenbare grunner er denne varianten mer fleksibel, gir bedre innkapsling av komponentene, og gjør det lettere å erstatte lavnivåkomponentene.

Selvfølgelig implementerer denne varianten av mønsteret bare å plassere Kundeservice, KartKundeDao, og CustomerDao i separate pakker.

Derfor er et diagram tilstrekkelig for å vise hvordan hver komponent er lagt ut med denne implementeringen:

5. Java 11 Modular Implementation

Det er ganske enkelt å omformulere demo-applikasjonen til en modulær.

Dette er en veldig fin måte å demonstrere hvordan JPMS håndhever beste programmeringspraksis, inkludert sterk innkapsling, abstraksjon og gjenbruk av komponenter gjennom DIP.

Vi trenger ikke å implementere prøvekomponentene våre på nytt fra bunnen av. Derfor, modulering av eksempelsøknaden vår er bare å plassere hver komponentfil i en egen modul, sammen med den tilsvarende modulbeskrivelsen.

Slik ser den modulære prosjektstrukturen ut:

prosjektbasekatalog (kan være hva 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. Komponentmodulen på høyt nivå

La oss starte med å plassere Kundeservice klasse i sin egen modul.

Vi oppretter denne modulen i rotkatalogen com.baeldung.dip.services, og legg til modulbeskrivelsen, module-info.java:

modul com.baeldung.dip.services {krever com.baeldung.dip.entities; krever com.baeldung.dip.daos; bruker com.baeldung.dip.daos.CustomerDao; eksport com.baeldung.dip.services; }

Av åpenbare grunner vil vi ikke gå inn i detaljene om hvordan JPMS fungerer. Likevel er det klart å se modulavhengighetene bare ved å se på krever direktiver.

Den mest relevante detalj som er verdt å merke seg her er bruker direktivet. Det står det modulen er en klientmodul som forbruker en implementering av CustomerDao grensesnitt.

Selvfølgelig trenger vi fortsatt å plassere komponenten på høyt nivå, den Kundeservice klasse, i denne modulen. Så innenfor rotkatalogen com.baeldung.dip.tjenester, la oss lage følgende pakkelignende katalogstruktur: com / baeldung / dip / services.

Til slutt, la oss plassere CustomerService.java filen i den katalogen.

5.2. Abstraksjonsmodulen

På samme måte må vi plassere CustomerDao grensesnitt i sin egen modul. La oss derfor opprette modulen i rotkatalogen com.baeldung.dip.daos, og legg til modulbeskrivelsen:

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

La oss nå navigere til com.baeldung.dip.daos katalog og opprett følgende katalogstruktur: com / baeldung / dip / daos. La oss plassere CustomerDao.java filen i den katalogen.

5.3. Komponentmodulen på lavt nivå

Logisk sett må vi sette komponenten på lavt nivå, SimpleCustomerDao, i en egen modul også. Som forventet ser prosessen veldig ut som vi nettopp gjorde med de andre modulene.

La oss lage den nye modulen i rotkatalogen com.baeldung.dip.daoimplementations, og inkluderer modulbeskrivelsen:

modul com.baeldung.dip.daoimplementations {krever com.baeldung.dip.entities; krever com.baeldung.dip.daos; gir com.baeldung.dip.daos.CustomerDao com.baeldung.dip.daoimplementations.SimpleCustomerDao; eksport com.baeldung.dip.daoimplementations; }

I JPMS-sammenheng dette er en tjenesteleverandørmodul, siden det erklærer gir og med direktiver.

I dette tilfellet lager modulen CustomerDao tjenesten tilgjengelig for en eller flere forbrukermoduler, gjennom SimpleCustomerDao gjennomføring.

La oss huske at forbrukermodulen vår, com.baeldung.dip.tjenester, bruker denne tjenesten gjennom bruker direktivet.

Dette viser tydelig hvor enkelt det er å ha en direkte DIP-implementering med JPMS, ved å bare definere forbrukere, tjenesteleverandører og abstraksjoner i forskjellige moduler.

På samme måte må vi plassere SimpleCustomerDao.java filen i denne nye modulen. La oss navigere til com.baeldung.dip.daoimplementations katalog, og opprett en ny pakkelignende katalogstruktur med dette navnet: com / baeldung / dip / daoimplementations.

Til slutt, la oss plassere SimpleCustomerDao.java filen i katalogen.

5.4. Enhetsmodulen

I tillegg må vi lage en annen modul der vi kan plassere Customer.java klasse. Som vi gjorde før, la oss lage rotkatalogen com.baeldung.dip.entities og inkluderer modulbeskrivelsen:

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

La oss opprette katalogen i pakkens rotkatalog com / baeldung / dip / entities og legg til følgende Customer.java fil:

offentlig klasse Kunde {privat slutt Strengnavn; // standard konstruktør / getter / toString}

5.5. Hovedapplikasjonsmodulen

Deretter må vi lage en ekstra modul som lar oss definere inngangspunktet til demo-applikasjonen. La oss derfor opprette en annen rotkatalog com.baeldung.dip.mainapp og plasser modulbeskriveren i den:

modul com.baeldung.dip.mainapp {krever com.baeldung.dip.entities; krever com.baeldung.dip.daos; krever com.baeldung.dip.daoimplementations; krever com.baeldung.dip.services; eksporterer com.baeldung.dip.mainapp; }

La oss nå navigere til modulens rotkatalog, og opprette følgende katalogstruktur: com / baeldung / dip / mainapp. I den katalogen, la oss legge til en MainApplication.java fil, som ganske enkelt 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 (nye SimpleCustomerDao (kunder)); customerService.findAll (). forEach (System.out :: println); }}

Til slutt, la oss kompilere og kjøre demo-applikasjonen - enten fra IDE eller fra en kommandokonsoll.

Som forventet, bør vi se en liste over Kunde gjenstander som er skrevet ut til konsollen når applikasjonen starter:

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

I tillegg viser følgende diagram avhengighet av hver modul i applikasjonen:

6. Konklusjon

I denne veiledningen, Vi tok et dypdykk i DIPs nøkkelkonsepter, og vi viste også forskjellige implementeringer av mønsteret i Java 8 og Java 11, hvor sistnevnte bruker JPMS.

Alle eksemplene for implementering av Java 8 DIP og implementering av Java 11 er tilgjengelige på GitHub.


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