Princíp závislosti závislosti v Jave

1. Prehľad

Princíp závislosti závislostí (DIP) je súčasťou zbierky princípov objektovo orientovaného programovania, ktoré sú populárne známe ako SOLID.

Z holých kostí je DIP jednoduchá, ale výkonná paradigma programovania, ktorú môžeme použiť implementovať dobre štruktúrované, vysoko oddelené a opakovane použiteľné softvérové ​​komponenty.

V tomto návode preskúmame rôzne prístupy k implementácii DIP - jeden v prostredí Java 8 a druhý v prostredí Java 11 pomocou JPMS (Java Platform Module System).

2. Vkladanie závislostí a inverzia riadenia nie sú implementáciami DIP

V prvom rade urobme zásadné rozlíšenie, aby sme získali správne základy: DIP nie je ani injektáž závislosti (DI), ani inverzia riadenia (IoC). Napriek tomu všetci spolu fungujú skvele.

Zjednodušene povedané, DI je o tom, aby softvérové ​​komponenty výslovne deklarovali svoje závislosti alebo spolupracovníkov prostredníctvom svojich rozhraní API, namiesto toho, aby ich získavali sami.

Bez DI sú softvérové ​​komponenty navzájom pevne spojené. Preto je ťažké ich znova použiť, vymeniť, zosmiešniť a vyskúšať, čo má za následok rigidné vzory.

V prípade DI sa zodpovednosť za poskytovanie závislostí komponentov a grafov objektov zapojenia prenáša z komponentov na základný injekčný rámec. Z tohto pohľadu je DI iba spôsob, ako dosiahnuť IoC.

Na druhej strane, IoC je vzor, ​​v ktorom je riadenie toku aplikácie obrátené. Pri tradičných metodikách programovania má náš vlastný kód kontrolu nad tokom aplikácie. Naopak, s IoC sa kontrola prenáša na externý rámec alebo kontajner.

Rámec je rozšíriteľný kódový základ, ktorý definuje háčkové body pre zapojenie nášho vlastného kódu.

Rámec zase zavolá náš kód pomocou jednej alebo viacerých špecializovaných podtried, pomocou implementácií rozhraní a prostredníctvom anotácií. Jarný rámec je pekným príkladom tohto posledného prístupu.

3. Základy DIP

Aby sme pochopili motiváciu, ktorá stojí za DIP, začnime s jeho formálnou definíciou, ktorú uviedol Robert C. Martin vo svojej knihe, Agilný vývoj softvéru: Princípy, vzory a postupy:

  1. Moduly vysokej úrovne by nemali závisieť od modulov nízkej úrovne. Obidve by mali závisieť od abstrakcií.
  2. Abstrakcie by nemali závisieť od detailov. Podrobnosti by mali závisieť od abstrakcií.

Je teda zrejmé, že v jadre DIP je o obracaní klasickej závislosti medzi komponentmi na vysokej a nízkej úrovni abstrahovaním od interakcie medzi nimi.

Pri tradičnom vývoji softvéru závisia komponenty vyššej úrovne od komponentov nízkej úrovne. Preto je ťažké znovu použiť komponenty na vysokej úrovni.

3.1. Možnosti dizajnu a DIP

Uvažujme o jednoduchom StringProcessor trieda, ktorá dostane a String hodnota pomocou a StringReader komponent a napíše to niekde inde pomocou a StringWriter zložka:

public class StringProcessor {private final StringReader stringReader; súkromná konečná StringWriter stringWriter; public StringProcessor (StringReader stringReader, StringWriter stringWriter) {this.stringReader = stringReader; this.stringWriter = stringWriter; } public void printString () {stringWriter.write (stringReader.getValue ()); }} 

Aj keď implementácia StringProcessor trieda je základná, existuje niekoľko možností návrhu, ktoré tu môžeme urobiť.

Poďme každú voľbu dizajnu rozdeliť do samostatných položiek, aby sme jasne pochopili, ako môže každý mať vplyv na celkový dizajn:

  1. StringReader a StringWriter, komponenty nízkej úrovne, sú betónové triedy umiestnené v rovnakom balíku.StringProcessor, komponent vysokej úrovne je umiestnený v inom obale. StringProcessor záleží na StringReader a StringWriter. Neexistuje teda inverzia závislostí StringProcessor nie je možné opakovane použiť v inom kontexte.
  2. StringReader a StringWriter sú rozhrania umiestnené v rovnakom balíku spolu s implementáciami. StringProcessor teraz záleží na abstrakciách, ale komponenty nízkej úrovne nie. Inverziu závislostí sme zatiaľ nedosiahli.
  3. StringReader a StringWriter sú rozhrania umiestnené v rovnakom balíku spolu s StringProcessor. Teraz, StringProcessor má výslovné vlastníctvo abstrakcií. StringProcessor, StringReader, a StringWriter všetko závisí od abstrakcií. Inverziu závislostí zhora nadol sme dosiahli abstrahovaním interakcie medzi komponentmi.StringProcessor je teraz opakovane použiteľný v inom kontexte.
  4. StringReader a StringWriter sú rozhrania umiestnené v samostatnom balíku od StringProcessor. Dosiahli sme inverziu závislostí a je tiež jednoduchšie ju nahradiť StringReader a StringWriter implementácie. StringProcessor je tiež opakovane použiteľný v inom kontexte.

Zo všetkých vyššie uvedených scenárov sú platnými implementáciami DIP iba ​​položky 3 a 4.

3.2. Definovanie vlastníctva abstrakcií

Položka 3 je priama implementácia DIP, kde sú komponent na vysokej úrovni a abstrakcie umiestnené v rovnakom balíku. Teda zložka na vysokej úrovni vlastní abstrakcie. V tejto implementácii je komponent na vysokej úrovni zodpovedný za definovanie abstraktného protokolu, prostredníctvom ktorého interaguje s komponentmi na nízkej úrovni.

Rovnako je položka 4 viac oddelenou implementáciou DIP. V tomto variante vzoru ani zložka na vysokej, ani na nízkej úrovni nevlastní abstrakcie.

Abstrakcie sú umiestnené v samostatnej vrstve, čo uľahčuje prepínanie nízkoúrovňových komponentov. Zároveň sú všetky komponenty navzájom izolované, čo vedie k silnejšej enkapsulácii.

3.3. Výber správnej úrovne abstrakcie

Vo väčšine prípadov by výber abstrakcií, ktoré budú používať komponenty na vysokej úrovni, mal byť celkom priamy, ale s jednou výhradou, ktorú treba spomenúť: úrovňou abstrakcie.

Vo vyššie uvedenom príklade sme použili DI na injekciu a StringReader zadajte do StringProcessor trieda. To by bolo efektívne pokiaľ je úroveň abstrakcie StringReader sa nachádza v doméne StringProcessor.

Naopak, chýbali by nám iba vnútorné výhody DIP, ak StringReader je napríklad a Súbor objekt, ktorý číta a String hodnota zo súboru. V takom prípade úroveň odberu StringReader by bola oveľa nižšia ako úroveň domény StringProcessor.

Zjednodušene povedané úroveň abstrakcie, ktorú budú komponenty na vysokej úrovni používať na spoluprácu s komponentmi na nízkej úrovni, by mala byť vždy blízka doméne pôvodných.

4. Implementácie Java 8

Už sme sa pozreli do hĺbky na kľúčové koncepty DIP, takže teraz preskúmame niekoľko praktických implementácií vzoru v Jave 8.

4.1. Priama implementácia DIP

Vytvorme demo aplikáciu, ktorá načíta niektorých zákazníkov z vrstvy vytrvalosti a spracuje ich nejakým ďalším spôsobom.

Základným úložiskom vrstvy je zvyčajne databáza, ale aby bol kód jednoduchý, použijeme obyčajný kód Mapa.

Začnime tým definovanie zložky na vysokej úrovni:

verejná trieda CustomerService {private final CustomerDao customerDao; // štandardný konštruktor / getter public Voliteľné findById (int id) {return customerDao.findById (id); } public List findAll () {return customerDao.findAll (); }}

Ako vidíme, Zákaznícka služba trieda implementuje findById () a findAll () metódy, ktoré získavajú zákazníkov z vrstvy perzistencie pomocou jednoduchej implementácie DAO. Samozrejme sme mohli do triedy zapuzdriť viac funkcií, ale nechajme si to kvôli jednoduchosti takto.

V tomto prípade, the CustomerDao typu je abstrakcia že Zákaznícka služba používa na konzumáciu nízkoúrovňového komponentu.

Pretože ide o priamu implementáciu DIP, definujme abstrakciu ako rozhranie v rovnakom balíku Zákaznícka služba:

verejné rozhranie CustomerDao {voliteľné findById (int id); Zoznam findAll (); } 

Umiestnením abstrakcie do rovnakého balíka komponentu na vysokej úrovni dávame komponentu zodpovednosť za vlastníctvo abstrakcie. Tento detail implementácie je čo skutočne invertuje závislosť medzi zložkou na vysokej úrovni a zložkou na nízkej úrovni.

Navyše, úroveň odberu CustomerDao je blízko k jednému z Zákaznícka služba, čo je tiež potrebné pre dobrú implementáciu DIP.

Poďme si teraz vytvoriť komponent nízkej úrovne v inom balíku. V tomto prípade je to iba základ CustomerDao implementácia:

verejná trieda SimpleCustomerDao implementuje CustomerDao {// štandardný konštruktor / geter @Override public Voliteľné findById (int id) {návrat Optional.ofNullable (customers.get (id)); } @Override public List findAll () {return new ArrayList (customers.values ​​()); }}

Nakoniec vytvoríme test jednotky na kontrolu Zákaznícka služba funkcionalita triedy:

@Before public void setUpCustomerServiceInstance () {var customers = new HashMap (); customers.put (1, nový zákazník („John“)); customers.put (2, nový zákazník („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)); Zákazník zákazník = customerService.findById (1) .orElseGet (() -> nový zákazník („neexistujúci zákazník“)); assertThat (customer.getName ()). isEqualTo ("Neexistujúci zákazník"); }

Jednotkový test vykonáva Zákaznícka služba API. A ukazuje tiež, ako manuálne vložiť abstrakciu do komponentu na vysokej úrovni. Vo väčšine prípadov by sme to dosiahli použitím nejakého druhu DI kontajnera alebo rámca.

Nasledujúci diagram ďalej zobrazuje štruktúru našej ukážkovej aplikácie z pohľadu balíka na vysokej a nízkej úrovni:

4.2. Alternatívna implementácia DIP

Ako sme už diskutovali, je možné použiť alternatívnu implementáciu DIP, kedy komponenty na vysokej úrovni, abstrakcie a komponenty na nízkej úrovni umiestnime do rôznych balíkov.

Zo zrejmých dôvodov je tento variant flexibilnejší, poskytuje lepšie zapuzdrenie komponentov a uľahčuje výmenu komponentov nižšej úrovne.

Implementácia tohto variantu vzoru sa samozrejme scvrkáva iba na umiestnenie Zákaznícka služba, MapCustomerDao, a CustomerDao v samostatných baleniach.

Preto stačí diagram, ktorý ukazuje, ako je pri tejto implementácii usporiadaný každý komponent:

5. Modulárna implementácia Java 11

Je pomerne ľahké prepracovať našu ukážkovú aplikáciu na modulárnu.

Je to skutočne pekný spôsob, ako demonštrovať, ako JPMS vynúti najlepšie postupy programovania vrátane silného zapuzdrenia, abstrakcie a opätovného použitia komponentov prostredníctvom protokolu DIP.

Naše vzorové komponenty nemusíme od začiatku znova implementovať. Teda modularizácia našej vzorovej aplikácie je len otázkou umiestnenia každého súboru komponentu do samostatného modulu spolu s príslušným deskriptorom modulu.

Takto bude vyzerať modulárna štruktúra projektu:

základný adresár projektu (môže to byť čokoľvek, napríklad dipmodular) | - com.baeldung.dip.services module-info.java | - com | - baeldung | - dip | - služby CustomerService.java | - modul com.baeldung.dip.daos -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 | - entity Customer.java | - com.baeldung.dip.mainapp module-info.java | - com | - baeldung | - dip | - mainapp MainApplication.java 

5.1. Modul komponentov na vysokej úrovni

Začnime umiestnením Zákaznícka služba triedy vo vlastnom module.

Tento modul vytvoríme v koreňovom adresári com.baeldung.dip.services, a pridať deskriptor modulu, modul-info.java:

modul com.baeldung.dip.services {vyžaduje com.baeldung.dip.entities; vyžaduje com.baeldung.dip.daos; používa com.baeldung.dip.daos.CustomerDao; export com.baeldung.dip.services; }

Zo zrejmých dôvodov sa nebudeme venovať podrobnostiam o fungovaní JPMS. Aj napriek tomu je jasné, že závislosti modulov je možné vidieť už len pri pohľade na vyžaduje smernice.

Najdôležitejším detailom, ktorý tu stojí za zmienku, je používa smernice. Uvádza sa v ňom to modul je klientský modul ktorá spotrebúva implementáciu CustomerDao rozhranie.

Samozrejme, stále musíme umiestniť komponent na vysokej úrovni, Zákaznícka služba triedy, v tomto module. Takže v koreňovom adresári com.baeldung.dip.services, vytvorme nasledujúcu adresárovú štruktúru podobnú balíku: com / baeldung / dip / services.

Nakoniec umiestnime CustomerService.java súbor v danom adresári.

5.2. Abstrakčný modul

Rovnako musíme umiestniť CustomerDao rozhranie vo vlastnom module. Preto vytvorme modul v koreňovom adresári com.baeldung.dip.daosa pridajte deskriptor modulu:

modul com.baeldung.dip.daos {vyžaduje com.baeldung.dip.entities; export com.baeldung.dip.daos; }

Teraz prejdime na com.baeldung.dip.daos adresár a vytvorte nasledujúcu adresárovú štruktúru: com / baeldung / dip / daos. Umiestnime CustomerDao.java súbor v danom adresári.

5.3. Nízkoúrovňový komponentový modul

Logicky musíme dať nízkoúrovňový komponent, SimpleCustomerDao, tiež v samostatnom module. Podľa očakávania vyzerá tento proces veľmi podobne, ako sme to spravili s ostatnými modulmi.

Vytvorme nový modul v koreňovom adresári com.baeldung.dip.daoimplementationsa zahrňte deskriptor modulu:

modul com.baeldung.dip.daoimplementations {vyžaduje com.baeldung.dip.entities; vyžaduje com.baeldung.dip.daos; poskytuje com.baeldung.dip.daos.CustomerDao s com.baeldung.dip.daoimplementations.SimpleCustomerDao; export com.baeldung.dip.daoimplementations; }

V kontexte JPMS toto je modul poskytovateľa služieb, pretože vyhlasuje poskytuje a s smernice.

V takom prípade modul vytvorí CustomerDao služba dostupná pre jeden alebo viac spotrebiteľských modulov prostredníctvom SimpleCustomerDao implementácia.

Nezabúdajme, že náš spotrebiteľský modul com.baeldung.dip.services, spotrebúva túto službu prostredníctvom používa smernice.

Toto jasne ukazuje aké jednoduché je mať priamu implementáciu DIP s JPMS, a to iba definovaním spotrebiteľov, poskytovateľov služieb a abstrakcií v rôznych moduloch.

Rovnako musíme umiestniť SimpleCustomerDao.java súbor v tomto novom module. Prejdime k com.baeldung.dip.daoimplementations adresár a vytvorte novú adresárovú štruktúru podobnú balíku s týmto názvom: com / baeldung / dip / daoimplementations.

Nakoniec umiestnime SimpleCustomerDao.java súbor v adresári.

5.4. Modul entity

Ďalej musíme vytvoriť ďalší modul, do ktorého môžeme umiestniť Zákazník.java trieda. Rovnako ako predtým sme vytvorili koreňový adresár com.baeldung.dip.entities a zahrnúť deskriptor modulu:

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

V koreňovom adresári balíka vytvorme adresár com / baeldung / dip / entity a pridať nasledujúce Zákazník.java spis:

public class Zákazník {private final Názov reťazca; // štandardný konštruktor / getter / toString}

5.5. Hlavný aplikačný modul

Ďalej musíme vytvoriť ďalší modul, ktorý nám umožní definovať vstupný bod našej ukážkovej aplikácie. Vytvorme preto ďalší koreňový adresár com.baeldung.dip.mainapp a vložte do nej deskriptor modulu:

modul com.baeldung.dip.mainapp {vyžaduje com.baeldung.dip.entities; vyžaduje com.baeldung.dip.daos; vyžaduje com.baeldung.dip.daoimplementations; vyžaduje com.baeldung.dip.services; export com.baeldung.dip.mainapp; }

Teraz prejdime do koreňového adresára modulu a vytvorme nasledujúcu adresárovú štruktúru: com / baeldung / dip / mainapp. V tomto adresári pridajme a MainApplication.java súbor, ktorý jednoducho implementuje a hlavný() metóda:

public class MainApplication {public static void main (String args []) {var customers = new HashMap (); customers.put (1, nový zákazník („John“)); customers.put (2, nový zákazník („Susan“)); CustomerService customerService = nový CustomerService (nový SimpleCustomerDao (zákazníci)); customerService.findAll (). forEach (System.out :: println); }}

Na záver poďme zostaviť a spustiť ukážkovú aplikáciu - buď z nášho IDE, alebo z príkazovej konzoly.

Ako sa dalo očakávať, mali by sme vidieť zoznam Zákazník objekty vytlačené na konzolu pri spustení aplikácie:

Zákazník {name = John} Zákazník {name = Susan} 

Nasledujúca schéma navyše zobrazuje závislosti každého modulu aplikácie:

6. Záver

V tomto návode podrobne sme sa ponorili do kľúčových konceptov DIP a ukázali sme tiež rôzne implementácie vzoru v jazykoch Java 8 a Java 11, pričom druhý používa JPMS.

Všetky príklady implementácie Java 8 DIP a implementácie Java 11 sú k dispozícii na GitHub.


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