She wants the DI
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. Amikor 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.
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ő):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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.
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.
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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.
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.