A Java függőségi inverziós elve

1. Áttekintés

A Dependency Inversion Principle (DIP) az objektum-orientált programozási elvek gyűjteményének része, amely közismert nevén SOLID.

A csupasz csontoknál a DIP egy egyszerű - ugyanakkor hatékony - programozási paradigma, amelyet felhasználhatunk jól strukturált, erősen leválasztott és újrafelhasználható szoftverkomponensek megvalósítására.

Ebben az oktatóanyagban megvizsgáljuk a DIP megvalósításának különböző megközelítéseit - egyet a Java 8-ban és egyet a Java 11-ben a JPMS (Java Platform Module System) segítségével.

2. A függőség injektálása és a vezérlés inverziója nem DIP megvalósítás

Először is tegyünk alapvető különbséget az alapok helyes megértése érdekében: a DIP sem függőségi injektálás (DI), sem a kontroll inverziója (IoC). Ennek ellenére mindannyian remekül működnek együtt.

Egyszerűen fogalmazva, a DI arról szól, hogy szoftverkomponenseket készítsen a függőségük vagy az együttműködők kifejezett deklarálására az API-k révén, ahelyett, hogy saját maguk szereznék be őket.

DI nélkül a szoftverkomponensek szorosan kapcsolódnak egymáshoz. Ezért nehéz újrafelhasználni, kicserélni, kigúnyolni és tesztelni őket, ami merev kialakítást eredményez.

A DI-vel az alkatrészfüggőségek és az objektumdiagramok bekötésének felelőssége átkerül az alkatrészekről az alapul szolgáló injektálási keretrendszerre. Ebből a szempontból a DI csak egy módszer az IoC elérésére.

Másrészről, Az IoC egy olyan minta, amelyben az alkalmazás folyamatának vezérlése megfordul. A hagyományos programozási módszertanokkal egyedi kódunk vezérli az alkalmazás folyamatát. Fordítva, az IoC-vel a vezérlés egy külső keretrendszerbe vagy tárolóba kerül.

A keretrendszer kiterjeszthető kódbázis, amely meghatározza a saját kódunk csatlakoztatásához szükséges horogpontokat.

Viszont a keretrendszer visszahívja kódunkat egy vagy több speciális alosztályon keresztül, az interfészek implementációinak felhasználásával és annotációkkal. A tavaszi keret szép példa erre az utolsó megközelítésre.

3. A DIP alapjai

A DIP mögött meghúzódó motiváció megértéséhez kezdjük annak formális meghatározásával, amelyet Robert C. Martin adott könyvében, Agilis szoftverfejlesztés: alapelvek, minták és gyakorlatok:

  1. A magas szintű modulok nem függhetnek az alacsony szintű moduloktól. Mindkettőnek függnie kell az absztrakciótól.
  2. Az absztrakciók nem függhetnek a részletektől. A részleteknek az absztrakciótól kell függeniük.

Tehát egyértelmű, hogy a lényege, a DIP a magas és az alacsony szintű komponensek közötti klasszikus függőség megfordításáról szól, elvonva a köztük lévő interakciót.

A hagyományos szoftverfejlesztésben a magas szintű összetevők az alacsony szintűektől függenek. Így nehéz újrafelhasználni a magas szintű alkatrészeket.

3.1. Design Choices és a DIP

Vegyünk egy egyszerűt StringProcessor osztály, amely a Húr érték a használatával StringReader komponenst, és máshova írja az a használatával StringWriter összetevő:

public class StringProcessor {private final StringReader stringReader; privát végleges StringWriter stringWriter; public StringProcessor (StringReader stringReader, StringWriter stringWriter) {this.stringReader = stringReader; this.stringWriter = stringWriter; } public void printString () {stringWriter.write (stringReader.getValue ()); }} 

Bár a StringProcessor osztály alapvető, többféle tervezési lehetőséget is megtehetünk itt.

Bontsuk szét az egyes tervezési lehetőségeket külön elemekre, hogy világosan megértsük, hogyan befolyásolhatják ezek az általános tervezést:

  1. StringReader és StringWriter, az alacsony szintű alkatrészek ugyanabba a csomagba helyezett betonosztályok.StringProcessor, a magas szintű alkatrészt egy másik csomagba helyezzük. StringProcessor attól függ StringReader és StringWriter. Nincs tehát a függőségek inverziója StringProcessor más kontextusban nem használható fel újra.
  2. StringReader és StringWriter olyan interfészek, amelyek a megvalósításokkal együtt ugyanabban a csomagban vannak elhelyezve. StringProcessor most az absztrakcióktól függ, de az alacsony szintű komponensek nem. Még nem értük el a függőségek inverzióját.
  3. StringReader és StringWriter olyan interfészek, amelyek ugyanabban a csomagban vannak elhelyezve StringProcessor. Most, StringProcessor kifejezetten az absztrakciók tulajdonosa. StringProcessor, StringReader, és StringWriter minden az absztrakciótól függ. Az összetevők közötti interakció elvonatkoztatásával elértük a függőségek fentről lefelé történő inverzióját.StringProcessor most más kontextusban újrafelhasználható.
  4. StringReader és StringWriter olyan interfészek, amelyek külön csomagból vannak elhelyezve StringProcessor. Elértük a függőségek inverzióját, és azt is könnyebb pótolni StringReader és StringWriter megvalósítások. StringProcessor más kontextusban is újrafelhasználható.

A fenti forgatókönyvek közül csak a 3. és a 4. tétel a DIP érvényes megvalósítása.

3.2. Az absztrakciók tulajdonjogának meghatározása

A 3. pont közvetlen DIP megvalósítás, ahol a magas szintű komponens és az absztrakció (k) ugyanabba a csomagba kerülnek. Ennélfogva, a magas szintű komponens az absztrakciók tulajdonosa. Ebben a megvalósításban a magas szintű komponens felelős annak az elvont protokollnak a meghatározásáért, amelyen keresztül kölcsönhatásba lép az alacsony szintű komponensekkel.

Hasonlóképpen, a 4. tétel egy szétválasztottabb DIP megvalósítás. A minta ezen változatában sem a magas szintű, sem az alacsony szintű nem rendelkezik az absztrakciók tulajdonjogaival.

Az absztrakciókat külön rétegbe helyezzük, ami megkönnyíti az alacsony szintű komponensek váltását. Ugyanakkor az összes komponenst elkülönítik egymástól, ami erősebb kapszulázást eredményez.

3.3. A megfelelő absztrakciós szint kiválasztása

A legtöbb esetben azoknak az absztrakcióknak a megválasztása, amelyeket a magas szintű komponensek használni fognak, meglehetősen egyszerűnek kell lennie, de egy figyelmeztetéssel érdemes megjegyezni: az absztrakció szintjét.

A fenti példában DI-t használtunk a StringReader írja be a StringProcessor osztály. Ez hatékony lenne amíg az absztrakció szintje StringReader közel áll a StringProcessor.

Ezzel szemben csak hiányolnánk a DIP belső előnyeit, ha StringReader például a File objektum, amely olvassa a Húr érték egy fájlból. Ebben az esetben az absztrakció szintje StringReader sokkal alacsonyabb lenne, mint a StringProcessor.

Egyszerűen szólva, az absztrakció szintjének, amelyet a magas szintű komponensek használni fognak az alacsony szintű komponensekkel való együttműködéshez, mindig közel kell lennie az előbbiek tartományához.

4. Java 8 implementációk

Már alaposan megvizsgáltuk a DIP kulcsfontosságú fogalmait, ezért most a Java 8 mintájának néhány gyakorlati megvalósítását vizsgáljuk meg.

4.1. Közvetlen DIP megvalósítás

Hozzunk létre egy bemutató alkalmazást, amely lekér néhány ügyfelet a perzisztencia rétegből, és további módon feldolgozza őket.

A réteg mögöttes tárolója általában adatbázis, de hogy a kód egyszerű legyen, itt egy sima szót fogunk használni Térkép.

Kezdjük azzal meghatározza a magas szintű összetevőt:

public class CustomerService {private final CustomerDao customerDao; // standard konstruktor / getter public Opcionális findById (int id) {return customerDao.findById (id); } public list findAll () {return customerDao.findAll (); }}

Mint láthatjuk, a Vevőszolgálat osztály hajtja végre a findById () és Találd meg mindet() metódusok, amelyek egy egyszerű DAO implementáció segítségével lekérik az ügyfeleket a perzisztencia rétegből. Természetesen több funkciót is be tudtunk volna foglalni az osztályba, de az egyszerűség kedvéért tartsuk meg így.

Ebben az esetben, a CustomerDao típus az absztrakció hogy Vevőszolgálat felhasználja az alacsony szintű alkatrész fogyasztására.

Mivel ez közvetlen DIP megvalósítás, definiáljuk az absztrakciót interfészként ugyanabban a csomagban Vevőszolgálat:

nyilvános felület CustomerDao {Opcionális findById (int id); List findAll (); } 

Azáltal, hogy az absztrakciót a magas szintű komponens ugyanabba a csomagjába helyezzük, az összetevőt felelőssé tesszük az absztrakció birtoklásáért. Ez a megvalósítás részlete ami valójában megfordítja a függőséget a magas szintű és az alacsony szintű komponens között.

Továbbá, absztrakció szintje CustomerDao közel áll a Vevőszolgálat, ami szintén szükséges a jó DIP megvalósításhoz.

Most hozzuk létre az alacsony szintű komponenst egy másik csomagban. Ebben az esetben ez csak egy alap CustomerDao végrehajtás:

public class SimpleCustomerDao implementálja a CustomerDao {// standard constructor / getter @Override public Opcionális findById (int id) {return Optional.ofNullable (customers.get (id)); } @Orride public List findAll () {return new ArrayList (customers.values ​​()); }}

Végül hozzunk létre egy egység tesztet a Vevőszolgálat osztály funkcionalitása:

@ Nyilvános void előtt setUpCustomerServiceInstance () {var customers = new HashMap (); customers.put (1, új Ügyfél ("John")); customers.put (2, új Ügyfél ("Susan")); customerService = új CustomerService (új SimpleCustomerDao (ügyfelek)); } @Test public void givenCustomerServiceInstance_whenCalledFindById_thenCorrect () {assertThat (customerService.findById (1)). IsInstanceOf (Opcionális.osztály); } @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 = új CustomerService (új SimpleCustomerDao (ügyfelek)); Ügyfél ügyfél = customerService.findById (1) .orElseGet (() -> új Ügyfél ("Nem létező ügyfél")); assertThat (customer.getName ()). isEqualTo ("Nem létező ügyfél"); }

Az egység teszt gyakorolja a Vevőszolgálat API. Ez azt is megmutatja, hogyan lehet manuálisan beadni az absztrakciót a magas szintű komponensbe. A legtöbb esetben valamilyen DI tárolót vagy keretet használnánk ennek megvalósításához.

Ezenkívül a következő ábra bemutatja bemutató alkalmazásunk felépítését, magas szintű és alacsony szintű csomag szempontjából:

4.2. Alternatív DIP megvalósítás

Ahogy korábban megbeszéltük, lehetséges alternatív DIP megvalósítást használni, ahol a magas szintű komponenseket, az absztrakciókat és az alacsony szintűeket különböző csomagokba helyezzük.

Nyilvánvaló okokból ez a változat rugalmasabb, jobban beágyazza az alkatrészeket, és megkönnyíti az alacsony szintű alkatrészek cseréjét.

Természetesen a minta ezen változatának megvalósítása csupán elhelyezésre vezethető vissza Vevőszolgálat, MapCustomerDao, és CustomerDao külön csomagokban.

Ezért egy diagram elegendő annak bemutatására, hogy az egyes összetevők hogyan vannak elrendezve ezzel a megvalósítással:

5. Java 11 moduláris megvalósítás

Meglehetősen könnyű átalakítani a bemutató alkalmazásunkat modulárisra.

Ez egy nagyon jó módszer annak bemutatására, hogy a JPMS miként érvényesíti a legjobb programozási gyakorlatokat, ideértve az erős beágyazást, az absztrakciót és az alkatrészek újrafelhasználását a DIP-en keresztül.

Nem kell a nulláról újratölteni a mintakomponenseket. Ennélfogva, A mintaalkalmazásunk modulálása csupán annyit jelent, hogy az egyes komponensfájlokat külön modulba helyezzük, a megfelelő modulleíróval együtt.

Így néz ki a moduláris projektstruktúra:

projekt alapkönyvtár (bármi lehet, például dipmodular) | - com.baeldung.dip.services module-info.java | - com | - baeldung | - dip | - services -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 | - entitások Customer.java | - com.baeldung.dip.mainapp module-info.java | - com | - baeldung | - dip | - mainapp MainApplication.java 

5.1. A magas szintű alkatrész modul

Kezdjük azzal, hogy a Vevőszolgálat osztály a saját moduljában.

Ezt a modult a gyökérkönyvtárban fogjuk létrehozni com.baeldung.dip.services, és adja hozzá a modulleírót, module-info.java:

modul com.baeldung.dip.services {szükséges com.baeldung.dip.entities; com.baeldung.dip.daos szükséges; használja a com.baeldung.dip.daos.CustomerDao; exportálja a com.baeldung.dip.services szolgáltatást; }

Nyilvánvaló okokból nem térünk ki a JPMS működésének részleteire. Ennek ellenére egyértelmű, hogy a modulfüggőségeket csak a igényel irányelvek.

A legrelevánsabb részlet, amelyet érdemes itt megjegyezni, a használ irányelv. Azt állítja a modul egy kliens modul hogy felemészti a CustomerDao felület.

Természetesen még mindig el kell helyeznünk a magas szintű alkatrészt, a Vevőszolgálat osztály, ebben a modulban. Tehát a gyökérkönyvtárban com.baeldung.dip.services, hozzuk létre a következő csomagszerű könyvtárstruktúrát: com / baeldung / dip / services.

Végül tegyük a CustomerService.java fájlt abban a könyvtárban.

5.2. Az absztrakciós modul

Hasonlóképpen el kell helyeznünk a CustomerDao interfész a saját moduljában. Ezért hozzuk létre a modult a gyökérkönyvtárban com.baeldung.dip.daos, és adja hozzá a modulleírót:

modul com.baeldung.dip.daos {com.baeldung.dip.entities szükséges; exportja com.baeldung.dip.daos; }

Most keressük meg a com.baeldung.dip.daos könyvtárat, és hozza létre a következő könyvtárstruktúrát: com / baeldung / dip / daos. Helyezzük el a CustomerDao.java fájlt abban a könyvtárban.

5.3. Alacsony szintű alkatrész modul

Logikusan fel kell tennünk az alacsony szintű komponenst, SimpleCustomerDao, külön modulban is. A várakozásoknak megfelelően a folyamat nagyon hasonlít arra, amit a többi modullal csináltunk.

Hozzuk létre az új modult a gyökérkönyvtárban com.baeldung.dip.daoimplementations, és tartalmazza a modulleírót:

modul com.baeldung.dip.daoimplementations {megköveteli com.baeldung.dip.entities; com.baeldung.dip.daos szükséges; biztosítja a com.baeldung.dip.daos.CustomerDao szolgáltatást a com.baeldung.dip.daoimplementations.SimpleCustomerDao; exportál com.baeldung.dip.daoimplementations; }

JPMS kontextusban ez egy szolgáltató modul, mivel kijelenti a biztosítja és val vel irányelvek.

Ebben az esetben a modul elkészíti a CustomerDao - egy vagy több fogyasztói modul számára elérhető szolgáltatás a SimpleCustomerDao végrehajtás.

Ne feledjük, hogy a fogyasztói modulunk, com.baeldung.dip.services, ezt a szolgáltatást a használ irányelv.

Ez világosan mutatja milyen egyszerű a közvetlen DIP megvalósítás a JPMS-mel, csupán a fogyasztók, a szolgáltatók és az absztrakciók meghatározása a különböző modulokban.

Hasonlóképpen el kell helyeznünk a SimpleCustomerDao.java fájl ebben az új modulban. Navigáljunk a com.baeldung.dip.daoimplementations könyvtárat, és hozzon létre egy új csomagszerű könyvtárstruktúrát a következő névvel: com / baeldung / dip / daoimplementations.

Végül tegyük a SimpleCustomerDao.java fájl a könyvtárban.

5.4. Az Entitás modul

Ezenkívül létre kell hoznunk egy másik modult, ahová elhelyezhetjük a Ügyfél.java osztály. Ahogy korábban tettük, hozzuk létre a gyökérkönyvtárat com.baeldung.dip.entities és tartalmazza a modulleírót:

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

A csomag gyökérkönyvtárában hozzuk létre a könyvtárat com / baeldung / dip / entitások és adja hozzá a következőket Ügyfél.java fájl:

public class Ügyfél {private final String name; // standard konstruktor / getter / toString}

5.5. A fő alkalmazási modul

Ezután létre kell hoznunk egy további modult, amely lehetővé teszi számunkra, hogy meghatározzuk a demo alkalmazás belépési pontját. Ezért hozzunk létre egy másik gyökérkönyvtárat com.baeldung.dip.mainapp és tegye bele a modulleírót:

modul com.baeldung.dip.mainapp {com.baeldung.dip.entities szükséges; com.baeldung.dip.daos szükséges; com.baeldung.dip.daoimplementations szükséges; com.baeldung.dip.services szükséges; exportálja a com.baeldung.dip.mainapp; }

Most keresse meg a modul gyökérkönyvtárát, és hozza létre a következő könyvtárstruktúrát: com / baeldung / dip / mainapp. Ebben a könyvtárban adjunk hozzá egy a-t MainApplication.java fájl, amely egyszerűen végrehajtja a fő() módszer:

public class MainApplication {public static void main (String args []) {var customers = new HashMap (); customers.put (1, új Ügyfél ("John")); customers.put (2, új Ügyfél ("Susan")); CustomerService customerService = új CustomerService (új SimpleCustomerDao (ügyfelek)); customerService.findAll (). forEach (System.out :: println); }}

Végül fordítsuk le és futtassuk a bemutató alkalmazást - akár az IDE-nkből, akár a parancskonzolról.

A várakozásoknak megfelelően látnunk kell egy listát Vevő az alkalmazás indításakor a konzolra kinyomtatott objektumok:

Ügyfél {name = John} Ügyfél {name = Susan} 

Ezenkívül a következő ábra bemutatja az alkalmazás egyes moduljainak függőségeit:

6. Következtetés

Ebben az oktatóanyagban mélyen belemerültünk a DIP kulcsfontosságú koncepcióiba, és bemutattuk a minta különböző megvalósításait a Java 8 és a Java 11-ben, az utóbbival a JPMS segítségével.

A Java 8 DIP és a Java 11 implementáció összes példája elérhető a GitHubon.