Letscode.hu

… minden ami fejlesztés

She wants the DI

5 min read

A SOLID sorozatunk utolsó része következik, ahol a Dependency Inversion Principle, röviden DIP lesz terítéken. Alkalmazásunk felépítését tekintve többszintű hierarchiát képez. Ha megvizsgáljuk az osztályainkat és azok függőségeit, akkor egy faszerkezetet rajzolnak ki, ahol a fa gyökere felé haladva egyre magasabb és magasabb szintű osztályok következnek. tree-451x320Amikor tervezzük az alkalmazásainkat, a gyakorlat az, hogy először azokkal az alacsonyszintű osztályokkal kezdjük, amik az egyszerű műveleteket végzik, mint a lemezelérés, hálózati protokollok, stb. (Ezek lesznek a levelek) Ezután fogjuk ezek működését egységbe zárni magasabb szintű osztályainkba, ahol a komplex logika található (üzleti folyamatok, stb.).

Ahogy a sorrendből is lehet rá következtetni, ez utóbbiak függnek az alacsony szintű osztályoktól. Ebből kifolyólag az előbbi elgondolás, miszerint a low-level osztályoktól a high-levelek felé haladunk a megvalósítás során. A probléma itt a flexibilitás lesz, ugyanis ha az egyik alacsonyszintű osztályt le kell cserélni valahol, akkor meg vagyunk lőve, mert a magasabb szintű osztályainkat ennek implementációjára építettük.dip

Szemléltetésként nézzünk egy klasszikus példát a fenti esetre, amikor létrehozunk egy Copy modult, ami a billentyűzetről olvas be karaktereket és a nyomtatóra írja ki azokat. A magassztinű osztály, ami a logikát tartalmazza majd, a Copy lesz. Az alacsonyszintű pedig a KeyboardReader és PrinterWriter.

Ha a fentiek szerint terveztük meg a rendszerünket, akkor a magasszintű osztályba bele van betonozva a két alacsonyszintű implementáció. Ebben az esetben, ha meg akarnánk változatni a modul működését, hogy a nyomtató helyett egy fájlba írja azt, a FileWriter osztály segítségével, bizony bele kéne nyúlni a Copy osztályba (egy percre tételezzük fel, hogy rengeteg logikával van megáldva és nehezen tesztelhető):

class Copy {
     
     private $reader, $writer;

     public function __construct(KeyboardReader $reader, PrinterWriter $writer) {
          $this->reader = $reader;
          $this->writer = $writer;
     }
}

class KeyboardReader {
    public function read() {
        // cin >> rucsok
    }
}

class PrinterWriter {
    public function write() {
        // valami C alapú feketemágia
    }
}

class FileWriter { // sajna ezt sehol nem tudjuk használni még
    public function write() {
       // még több black magic
    }
}

Hát nem egyszerű a helyzet ugye? Megcsináltuk a kis FileWriter osztályunkat, átpasszoltuk a kollégánknak, de az nem tud vele mit kezdeni, maximum egy kis bájtkód manipulációval, amit a javások annyira szeretnek 🙂 Viszont nekünk nem állnak rendelkezésünkre ilyen finomságok, így kicsit újra kell gondolnunk az egészet.

bad design
Az alap felállás

Ha szeretnénk magunkat megkímélni a kedves kollégánk életreszóló haragjától, akkor nem árt bevezetni egy közbenső absztrakciós réteget a magas és alacsonyszintű osztályunk közé. Mivel a komplex logikát a magasszintű osztályok tartalmazzák, ezért nem szabadna, hogy alacsony szintű osztályoktól függjenek.

still bad design
Ez még mindig nem az igazi..

Az új réteg szintén gondot okozna, ha ezt is az alacsonyszintű osztályokra építenénk, ezért megfordítjuk a dolgot. Az új absztrakciós rétegre építjuk az alacsonyszintűeket és ezáltal a magasabbszintűeket is (Itt történik az a bizonyos dependency inversion). Na de mi lesz ez az absztrakciós réteg? Hát interfészek, mi más?:)

Akkor jöjjön a megvalósítás kódszinten:

interface Reader {
     public function read();
}

interface Writer {
     public function write();
}

class FileReader implements Reader {
   // ...
}

class FileWriter implements Writer {
   // ...
}

class KeyBoardReader implements Reader {
  // ...
}

class PrinterWriter implements Writer {

}

class Copy {
   private $reader, $writer;

   public function __construct(Reader $reader, Writer $writer) {
       $this->reader = $reader;
       $this->writer = $writer;
   }
}

A fenti példában kicsit túltoltam, mert plusz megvalósításokat írtam, de remélem jól látható, hogy mostantól mind a konkrét implementációk, mind pedig a magasabbszintű osztályunk az absztrakciós rétegtől, azaz a Reader és Writer interfészektől függ. Ha akarjuk, a Copy osztályunkban szabadon cserélgethetjük az említett interfészek implementációit amíg nem dobálunk NotImplementedExceptiont benne.

good design
A konkrét interfészek nélkül

Tehát a célt elértük, ami mi is volt?

  • A magas szintű modulok ne függjenek az alacsonyszintű moduloktól, hanem absztrakciókra építsük őket.
  • Az absztrakciók ne függjenek a megvalósítástól, hanem a megvalósítás függjön az absztrakcióktól.
  • A Copy osztályba nem kell belenyúljunk, ha egy újfajta implementációt adunk hozzá
  • Emiatt, hogy nem kell belenyúlni, a régi működést nem fogjuk hazavágni
  • Mivel nem módosult a Copy osztály, ezért nem kell újra unit tesztelni azt

Amikor alkalmazzuk ezt az elvet, akkor a magasszintű osztályok nem közvetlenül a megvalósító alacsonyszintű osztályokkal dolgoznak, hanem interfészeket fognak absztrakt rétegként használni. Emiatt, az új alacsonyszintű osztályok példányosítása a magasabbszintű osztályok belsejében a new kulcsszóval nem lehetséges, helyette Factory method, Abstract Factory, stb. létrehozási tervezési mintákat használhatunk e célra. Ahogy már a fenti példában is látszott, ennek az elvnek az alkalmazása plusz időráfordítást igényel, több osztályt, összeségében komplexebb kódbázist, ellenben sokkal flexibilisebb, mert nem égetünk bele megvalósításokat a kódba.

Természetesen ez sem egy aranyszabály, szóval ha van egy olyan osztályunk, amihez a következő évtizedben senki sem mer fog nyúlni, akkor nem szükséges ráerőszakolni ezt a mintát és átírni azt.

Copyright Letscode.hu 2014-2020 © Minden jog fenntartva. | Newsphere by AF themes.