Princip inverzije ovisnosti u Javi

1. Pregled

Princip inverzije ovisnosti (DIP) čini dio kolekcije objektno orijentiranih programskih principa popularno poznatih kao SOLID.

DIP je jednostavna, ali snažna programska paradigma koju možemo koristiti implementirati dobro strukturirane, visoko nerazdvojene i softverske komponente za višekratnu upotrebu.

U ovom vodiču, istražit ćemo različite pristupe za implementaciju DIP-a - jedan u Javi 8 i jedan u Javi 11 pomoću JPMS-a (Java Platform Module System).

2. Ubrizgavanje ovisnosti i inverzija kontrole nisu provedbe DIP-a

Prvo i najvažnije, napravimo temeljnu razliku kako bismo popravili osnove: DIP nije niti ubrizgavanje ovisnosti (DI) niti inverzija upravljanja (IoC). Unatoč tome, svi sjajno surađuju.

Jednostavno rečeno, DI se bavi izradom softverskih komponenata koje izričito izjavljuju svoje ovisnosti ili suradnike putem svojih API-ja, umjesto da ih same nabavljaju.

Bez DI-a, softverske su komponente međusobno čvrsto povezane. Stoga ih je teško ponovno upotrijebiti, zamijeniti, ismijati i testirati, što rezultira krutim dizajnom.

S DI, odgovornost za pružanje ovisnosti o komponentama i grafova objekata ožičenja prenosi se s komponenata na temeljni okvir ubrizgavanja. Iz te perspektive, DI je samo način za postizanje IoC-a.

S druge strane, IoC je obrazac u kojem je kontrola toka aplikacije obrnuta. Uz tradicionalne metodologije programiranja, naš prilagođeni kôd ima kontrolu nad protokom aplikacije. Obrnuto, s IoC-om se kontrola prenosi u vanjski okvir ili spremnik.

Okvir je proširiva baza kodova koja definira točke zakačenja za uključivanje vlastitog koda.

Zauzvrat, okvir poziva naš kôd kroz jednu ili više specijaliziranih potklasa, koristeći implementacije sučelja i putem bilješki. Proljetni okvir lijep je primjer ovog posljednjeg pristupa.

3. Osnove DIP-a

Da bismo razumjeli motivaciju za DIP-om, krenimo od njegove formalne definicije, koju je dao Robert C. Martin u svojoj knjizi, Agilni razvoj softvera: principi, obrasci i prakse:

  1. Moduli visoke razine ne bi trebali ovisiti o modulima niske razine. Oboje bi trebali ovisiti o apstrakcijama.
  2. Apstrakcije ne bi trebale ovisiti o pojedinostima. Detalji bi trebali ovisiti o apstrakcijama.

Dakle, jasno je da u srži, DIP govori o invertiranju klasične ovisnosti između komponenata visoke i niske razine apstrahirajući interakciju između njih.

U tradicionalnom razvoju softvera komponente visoke razine ovise o komponentama niske razine. Stoga je teško ponovno koristiti komponente visoke razine.

3.1. Izbori dizajna i DIP

Razmotrimo jednostavan StringProcessor razred koji dobiva Niz vrijednost pomoću a StringReader komponentu i zapisuje je negdje drugdje pomoću a StringWriter komponenta:

javna klasa StringProcessor {privatni konačni StringReader stringReader; privatni konačni StringWriter stringWriter; javni StringProcessor (StringReader stringReader, StringWriter stringWriter) {this.stringReader = stringReader; this.stringWriter = stringWriter; } javna void printString () {stringWriter.write (stringReader.getValue ()); }} 

Iako je provedba StringProcessor klasa je osnovna, ovdje možemo napraviti nekoliko izbora dizajna.

Razdvojimo svaki izbor dizajna na zasebne stavke kako bismo jasno razumjeli kako svaki od njih može utjecati na cjelokupni dizajn:

  1. StringReader i StringWriter, komponente na niskoj razini, betonske su klase smještene u isti paket.StringProcessor, komponenta visoke razine smještena je u drugi paket. StringProcessor ovisi o StringReader i StringWriter. Stoga nema inverzije ovisnosti StringProcessor nije za ponovnu upotrebu u drugom kontekstu.
  2. StringReader i StringWriter su sučelja smještena u isti paket zajedno s implementacijama. StringProcessor sada ovisi o apstrakcijama, ali komponente niske razine ne. Još nismo postigli inverziju ovisnosti.
  3. StringReader i StringWriter su sučelja smještena u isti paket zajedno s StringProcessor. Sada, StringProcessor ima izričito vlasništvo nad apstrakcijama. StringProcessor, StringReader, i StringWriter sve ovise o apstrakcijama. Inverziju ovisnosti od vrha do dna postigli smo apstrahirajući interakciju između komponenata.StringProcessor sada se može ponovno koristiti u drugom kontekstu.
  4. StringReader i StringWriter su sučelja smještena u zaseban paket od StringProcessor. Postigli smo inverziju ovisnosti, a također je i jednostavniju za zamjenu StringReader i StringWriter implementacije. StringProcessor također se može ponovno koristiti u drugom kontekstu.

Od svih gore navedenih scenarija, samo su stavke 3 i 4 valjane implementacije DIP-a.

3.2. Utvrđivanje vlasništva nad apstrakcijama

Točka 3. je izravna primjena DIP-a, pri čemu su komponenta visoke razine i apstrakcija (e) smještene u isti paket. Stoga, komponenta visoke razine posjeduje apstrakcije. U ovoj provedbi, komponenta visoke razine odgovorna je za definiranje apstraktnog protokola kroz koji komunicira s komponentama niske razine.

Isto tako, stavka 4. više je nevezana implementacija DIP-a. U ovoj varijanti uzorka, niti komponenta visoke razine niti ona niske razine nemaju vlasništvo nad apstrakcijama.

Apstrakcije su smještene u zaseban sloj, što olakšava prebacivanje komponenata niske razine. Istodobno su sve komponente međusobno izolirane, što daje jaču inkapsulaciju.

3.3. Odabir prave razine apstrakcije

U većini slučajeva odabir apstrakcija koje će koristiti komponente visoke razine trebao bi biti prilično jednostavan, ali uz jedno upozorenje vrijedno pažnje: razinu apstrakcije.

U gornjem primjeru koristili smo DI za ubrizgavanje a StringReader upišite u StringProcessor razred. Ovo bi bilo učinkovito sve dok je razina apstrakcije od StringReader je blizu domene StringProcessor.

Suprotno tome, samo bi nam nedostajale suštinske prednosti DIP-a ako StringReader je, na primjer, a Datoteka objekt koji čita a Niz vrijednost iz datoteke. U tom slučaju, razina apstrakcije od StringReader bio bi mnogo niži od razine domene StringProcessor.

Jednostavno rečeno, razina apstrakcije koju će komponente visoke razine koristiti za interakciju s onima niske razine trebala bi uvijek biti blizu domene prve.

4. Java 8 Implementacije

Već smo detaljno pogledali ključne koncepte DIP-a, pa ćemo sada istražiti nekoliko praktičnih implementacija uzorka u Javi 8.

4.1. Izravna implementacija DIP-a

Stvorimo demo aplikaciju koja dohvaća neke kupce iz sloja postojanosti i obrađuje ih na neki dodatni način.

Osnovna pohrana sloja obično je baza podataka, ali da bi kôd bio jednostavan, ovdje ćemo koristiti običan Karta.

Krenimo od definiranje komponente visoke razine:

javna klasa CustomerService {privatni konačni CustomerDao customerDao; // standardni konstruktor / getter javni Neobvezno findById (int id) {return customerDao.findById (id); } javni popis findAll () {return customerDao.findAll (); }}

Kao što vidimo, Služba za korisnike razred provodi findById () i findAll () metode koje dovode kupce iz sloja postojanosti pomoću jednostavne DAO implementacije. Naravno, mogli smo uvrstiti više funkcionalnosti u klasi, ali zadržimo je tako zbog jednostavnosti.

U ovom slučaju, the CustomerDao tip je apstrakcija da Služba za korisnike koristi za konzumiranje komponente niske razine.

Budući da je ovo izravna DIP implementacija, definirajmo apstrakciju kao sučelje u istom paketu Služba za korisnike:

javno sučelje CustomerDao {Neobvezno findById (int id); Popis findAll (); } 

Stavljanjem apstrakcije u isti paket komponente visoke razine, činimo komponentu odgovornom za posjedovanje apstrakcije. Ovaj detalj provedbe je ono što stvarno obrće ovisnost između komponente visoke i one niske razine.

U Dodatku, razina apstrakcije CustomerDao je blizu onome od Služba za korisnike, što je također potrebno za dobru implementaciju DIP-a.

Sada, kreirajmo komponentu niske razine u drugom paketu. U ovom je slučaju to samo osnovno CustomerDao provedba:

javna klasa SimpleCustomerDao implementira CustomerDao {// standardni konstruktor / getter @Override public Izborni findById (int id) {return Optional.ofNullable (customers.get (id)); } @Override javni popis findAll () {return new ArrayList (customers.values ​​()); }}

Na kraju, kreirajmo jedinstveni test za provjeru Služba za korisnike klasa 'funkcionalnost:

@Prije javne void setUpCustomerServiceInstance () {var kupci = novi HashMap (); customers.put (1, novi kupac ("John")); customers.put (2, novi kupac ("Susan")); customerService = nova CustomerService (nova SimpleCustomerDao (kupci)); } @Test javna praznina givenCustomerServiceInstance_whenCalledFindById_thenCorrect () {assertThat (customerService.findById (1)). IsInstanceOf (Optional.class); } @Test javna praznina givenCustomerServiceInstance_whenCalledFindAll_thenCorrect () {assertThat (customerService.findAll ()). IsInstanceOf (List.class); } @Test javna praznina givenCustomerServiceInstance_whenCalledFindByIdWithNullCustomer_thenCorrect () {var kupci = nova HashMap (); kupci.put (1, null); customerService = nova CustomerService (nova SimpleCustomerDao (kupci)); Kupac kupac = customerService.findById (1) .orElseGet (() -> novi kupac ("Nepostojeći kupac")); assertThat (customer.getName ()). isEqualTo ("Nepostojeći kupac"); }

Jedinstveni test vježba Služba za korisnike API. Također, pokazuje i kako ručno ubrizgati apstrakciju u komponentu visoke razine. U većini slučajeva za to bismo koristili neku vrstu DI spremnika ili okvira.

Uz to, sljedeći dijagram prikazuje strukturu naše demonstracijske aplikacije, od perspektive paketa visoke razine do niske razine:

4.2. Alternativna primjena DIP-a

Kao što smo ranije razgovarali, moguće je koristiti alternativnu implementaciju DIP-a, gdje komponente visoke razine, apstrakcije i one niske razine smještamo u različite pakete.

Iz očitih razloga, ova je varijanta fleksibilnija, daje bolju inkapsulaciju komponenata i olakšava zamjenu komponenata na niskoj razini.

Naravno, provođenje ove varijante uzorka svodi se na samo postavljanje Služba za korisnike, MapCustomerDao, i CustomerDao u zasebnim pakiranjima.

Stoga je dijagram dovoljan da pokaže kako je svaka komponenta postavljena ovom implementacijom:

5. Java 11 Modularna implementacija

Prilično je jednostavno preoblikovati našu demo aplikaciju u modularnu.

Ovo je zaista lijep način da pokažemo kako JPMS provodi najbolje prakse programiranja, uključujući snažnu inkapsulaciju, apstrakciju i ponovnu upotrebu komponenata putem DIP-a.

Ne trebamo ponovno primjenjivati ​​komponente uzorka od nule. Stoga, modulariziranje našeg uzorka aplikacije samo je pitanje postavljanja svake komponente datoteke u zasebni modul, zajedno s odgovarajućim deskriptorom modula.

Evo kako će izgledati modularna struktura projekta:

osnovni direktorij projekta (može biti bilo što, poput dipmodular) | - com.baeldung.dip.services module-info.java | - com | - baeldung | - dip | - usluge 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 | - entiteti Customer.java | - com.baeldung.dip.mainapp module-info.java | - com | - baeldung | - dip | - mainapp MainApplication.java 

5.1. Komponentni modul visoke razine

Počnimo s postavljanjem Služba za korisnike klase u vlastitom modulu.

Stvorit ćemo ovaj modul u korijenskom direktoriju com.baeldung.dip.services, i dodajte opis modula, module-info.java:

modul com.baeldung.dip.services {zahtijeva com.baeldung.dip.entities; zahtijeva com.baeldung.dip.daos; koristi com.baeldung.dip.daos.CustomerDao; izvozi com.baeldung.dip.services; }

Iz očitih razloga nećemo ulaziti u detalje o načinu rada JPMS-a. Bez obzira na to, jasno je vidjeti ovisnosti modula samo gledajući zahtijeva direktive.

Ovdje je najrelevantniji detalj vrijedan pažnje koristi direktiva. U njemu se navodi da modul je klijentski modul koji zahtijeva provedbu CustomerDao sučelje.

Naravno, još uvijek trebamo smjestiti komponentu visoke razine, Služba za korisnike klase, u ovom modulu. Dakle, unutar korijenskog direktorija com.baeldung.dip.services, kreirajmo sljedeću strukturu direktorija nalik paketu: com / baeldung / dip / services.

Napokon, postavimo CustomerService.java datoteku u tom direktoriju.

5.2. Modul apstrakcije

Isto tako, moramo postaviti CustomerDao sučelje u vlastitom modulu. Stoga, kreirajmo modul u korijenskom direktoriju com.baeldung.dip.daos, i dodajte opis modula:

modul com.baeldung.dip.daos {zahtijeva com.baeldung.dip.entities; izvozi com.baeldung.dip.daos; }

Idemo sada na com.baeldung.dip.daos direktorija i stvorite sljedeću strukturu direktorija: com / baeldung / dip / daos. Postavimo CustomerDao.java datoteku u tom direktoriju.

5.3. Komponentni modul niske razine

Logično, moramo staviti komponentu niske razine, SimpleCustomerDao, u zasebnom modulu. Očekivano, postupak izgleda vrlo slično onome što smo upravo radili s ostalim modulima.

Stvorimo novi modul u korijenskom direktoriju com.baeldung.dip.daoimplementacije, i uključuju opis modula:

modul com.baeldung.dip.daoimplementations {zahtijeva com.baeldung.dip.entities; zahtijeva com.baeldung.dip.daos; pruža com.baeldung.dip.daos.CustomerDao s com.baeldung.dip.daoimplementations.SimpleCustomerDao; izvozi com.baeldung.dip.daoimplementacije; }

U kontekstu JPMS-a, ovo je modul davatelja usluga, budući da proglašava pruža i s direktive.

U ovom slučaju, modul čini CustomerDao usluga dostupna jednom ili više potrošačkih modula putem SimpleCustomerDao provedba.

Imajmo na umu da naš potrošački modul, com.baeldung.dip.services, koristi ovu uslugu putem koristi direktiva.

To jasno pokazuje kako je jednostavno izravno implementirati DIP sa JPMS-om, samo definirajući potrošače, pružatelje usluga i apstrakcije u različitim modulima.

Isto tako, moramo postaviti SimpleCustomerDao.java datoteku u ovom novom modulu. Idemo na com.baeldung.dip.daoimplementacije direktorija i stvorite novu strukturu direktorija nalik paketu s ovim imenom: com / baeldung / dip / daoimplementations.

Napokon, postavimo SimpleCustomerDao.java datoteku u direktoriju.

5.4. Entitetni modul

Uz to, moramo stvoriti još jedan modul u koji možemo smjestiti Kupac.java razred. Kao i prije, kreirajmo korijenski direktorij com.baeldung.dip.entities i uključuju opis modula:

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

U osnovnom direktoriju paketa, kreirajmo direktorij com / baeldung / dip / entiteti i dodajte sljedeće Kupac.java datoteka:

kupac javne klase {privatni konačni naziv niza; // standardni konstruktor / getter / toString}

5.5. Glavni aplikacijski modul

Dalje, moramo stvoriti dodatni modul koji će nam omogućiti da definiramo ulaznu točku naše demo aplikacije. Stoga, kreirajmo još jedan korijenski direktorij com.baeldung.dip.mainapp i smjestite u njega deskriptor modula:

modul com.baeldung.dip.mainapp {zahtijeva com.baeldung.dip.entities; zahtijeva com.baeldung.dip.daos; zahtijeva com.baeldung.dip.daoimplementacije; zahtijeva com.baeldung.dip.services; izvozi com.baeldung.dip.mainapp; }

Idemo sada do korijenskog direktorija modula i stvorimo sljedeću strukturu direktorija: com / baeldung / dip / mainapp. U taj direktorij dodajte a MainApplication.java datoteku koja jednostavno implementira glavni() metoda:

javna klasa MainApplication {public static void main (String args []) {var kupci = novi HashMap (); customers.put (1, novi kupac ("John")); customers.put (2, novi kupac ("Susan")); CustomerService customerService = nova CustomerService (nova SimpleCustomerDao (kupci)); customerService.findAll (). forEach (System.out :: println); }}

Napokon, kompajlirajmo i pokrenimo demo aplikaciju - bilo iz našeg IDE-a ili iz naredbene konzole.

Kao što se očekivalo, trebali bismo vidjeti popis Kupac objekti ispisani na konzoli prilikom pokretanja aplikacije:

Kupac {name = John} Kupac {name = Susan} 

Uz to, sljedeći dijagram prikazuje ovisnosti svakog modula aplikacije:

6. Zaključak

U ovom vodiču, duboko smo zarobili ključne koncepte DIP-a, a također smo pokazali različite implementacije uzorka u Java 8 i Java 11, s tim da potonji koristi JPMS.

Svi primjeri implementacije Java 8 DIP i Java 11 dostupni su na GitHubu.