Sok tényező határozza meg azt, hogy valóban mérnöki minőségű-e a munkánk a programozás során, többek között a SOLID elvek alkalmazása az egyik ilyen. Nézzük meg, hogy teszik ezek minőségibbé kódjainkat.
Nézzük, miről lesz ma szó, mit jelent a clean code elvek alkalmazása során a SOLID elvek:
- Mik azok a SOLID elvek
- Single Responsibility – Az egy felelősség elve
- Open – Closed – Nyíltság – zártság elve
- Liskov féle helyettesítési elv
- Interface szegregáció elve
- Dependency Inversion elv
- +1 Dependency Inversion vs. Dependency Injection összehasonlítás
Mik azok a SOLID elvek, SOLID alapelvek
Mint az látható, az SOLID alapelvek szócska az 5 alapelv kezdőbetűiből tevődik össze. Sokszor találkozhatunk úgy vele, hogy az objektum orientált tervezés (OOD – Object Oriented Design) alapelvei. Mivel a Clean code a jól, szisztematikusan felépített, jól megtervezett kódot jelenti, így a Clean Code elvek betartásának szerves része.
A SOLID elvek nélkül is meglehet lenni, de nem érdemes. Hiába működőképes a kód ezen elvek alkalmazása nélkül is célszerű ezeket komolyan venni és e szerint fejleszteni a lentiek miatt:
- Ugye nem szeretnél „spagetti” fejlesztő lenni? Nem mindegy, hogy valaki ha meglátja a kódod mit mond. Nagyobb cégeknél, több éve a szakmában dolgozó mérnökök, ha olyan kódot látnak meg első napjukon, amiben semmilyen koncepció nincs, bizony kérdés nélkül még az első napon le is számolnak (megtörtént események alapján). Gondolj bele, neked is könnyebb egy minőségi kódot olvasni, mint egy 6000 soros, ömlesztett kódhalmazt, amit már maga a szerzője sem ért meg.
- A SOLID elvek nem betartása komoly kihátassal lehet a szoftverre is. Nem várt hibaesemények, leállások, memóriaszivárgások, magas hardverigény, sebezhetőség. Sajnos ezek általában a rendszer bevezetését követően jelentkeznek, amikor a nagyközönség elkezdi használni. Ilyenkor viszont már bajban lehetünk, újraírásra nincs erőforrás, így megindul a foltozgatás.
- Nem utolsó szempont, hogy milyen véleményen lesznek rólad az IT iparban a kódminőségedet illetően. Vannak programozók, akikkel nem szeretünk együtt dolgozni és vannak programozók, akiket mi ajánlunk be, hogy tanulhassunk tőlük. Egy kis szakmaisággal és odafigyeléssel Te is lehetsz az a programozó, aki után kapkodnak majd a cégek.
Single Responsibility – Az egy felelősség elve
Kezdjük rögtön egy TikTok tartalommal, ami kiemeli a lényegét ennek az elvnek. Talán az egyik legegyszerűbb elv, mégis sokan mellőzik, pedig egy kis időráfordítással máris szebb a kódunk.
Vegyünk egy sarkított példát az egy felelősség elvének bemutatására:
Tegyük fel, hogy éppen bevásárolsz. Vettél legalább 6 szatyor árut, amit gyalog valahogyan haza kell vinned. Vajon mi a kézenfekvőbb?
Bevállalni, hogy a 6 szatyrot egyszerre cipeljük magunkkal, megállva néhány méterenként pihenni, de útközben még a szatyor is leszakad. Pont van nálad egy pótszatyor, de össze kell szedni a kiömlött árut. Illetve még hazafelé beugrassz a postára is mert útba esik, illetve hazaérve még kiüríted a postaládát is.
Talán egyszerűbb a fenti műveletsor együttes, ha egyszerre csak 2 szatyornyi dolgot veszel meg, nem foglalkozol a postával és nem üríted a postaládát. Te most egy dologra koncentrálsz, hogy a két szatyor árut a lehető leggyorsabban hazavidd, hogy ne romoljon meg. A postaládát ki üríthetik a gyerek is, párod pedig leugrik a postára helyetted.
Mit jelent ez? A családban mindenki egy dologra összpontosít, nem téged zargatnak még a postával is. Viszont a bevásárlással kapcsolatban bármit kérdeznek, arra Te tudsz válaszolni, hiszen Te végezted ezt a feladatot.
Ugyanez történik a kódolás esetén is. Az egy felelősség elv alapján arra kell törekednünk, hogy egy objektum, az alkalmazás egy részegysége kizárólag egyetlen dologért legyen felelős. Ha azt esetleg módosítani is kell, akkor is csak egy feladat működőképességére kell összpontosítani, sokkal kisebb a lehetősége, hogy elrontunk már meglévő funkcionalitást.
Open Closed elv – Nyíltság/Zártság elve
Ehhez is van természetesen szemléltető TikTok video, amivel 30 másodperc alatt kideríthető ennek az elvnek a hasznossága.
Itt is vegyünk egy gyakorlati szemléltető példát, hogy mit is takar ez lényegében.
Tegyük fel, hogy lakberendő vagy. Van egy nappali, amit Te rendeztél be, de a méretre szabott TV állvány és nagyjából az egész fal „egyhangú” a megrendelő számára.
Azt egyből érezzük, hogy a méretre vágott, megrendelő elképzelései alapján legyártott bútorzatot ugyan lehetne farigcsálni, de nem érdemes. Ha mégis módosítjuk, egyáltalán nem biztos, hogy az már tetszeni fog a megrendelőnek.
Mit tudunk tenni? Dobjuk fel a meglévő megoldást, tegyünk kiegészítőket a TV állványra, mint például LED szalag, otthoni dekorációs kiegészítőket tegyünk a polcokra, esetleg néhány szobanövénnyel színesítsük a szabad felületeket.
Ugyebár sokkal egyszerűbb néhány apróbb kiegészítővel ékesíteni a polcrendszert, mint azt az egészet megmódosítani, ívelt vágni bele, formákat módosítani.
Ennek az elvnek is pont ugyanez a lényege. Ha már van egy régóta jól működő logikánk az alkalmazásban, akkor ne abba nyúljunk bele, ne azt módosítsuk, hanem egészítsük ki ezt a kérdéses kódrészt úgy, hogy elássa a szükséges feladatot. Így, mivel a meglévő megoldás nem módosul, sokkal kevesebb az esélye, hogy már korábban meglévő funkciók tönkre mennek.
Tehát kódjaink legyenek nyitottak a bővítésre, de zárkózzanak el a meglévő megoldások módosításától.
Liskov helyettesítési elve
Itt már kezd bonyolódni a dolog. Kevésbé kézzel fogható a történet, de próbáljuk meg ezt hétköznapi nyelven megfogalmazni.
Tegyük fel, hogy leklónoztak téged. Minden tekintetben van belőled mégegy. Már-már olyan megtévesztő a klónod, hogy még a párod is összekever vele. Azért jó ez a klón, mert magad helyett például el tudod küldeni bevásárolni a hipermarketbe. Te pedig el tudsz menni a helyi piacra és megvenni, amire szüksége van a családnak.
Itt a kulcspont, hogy neked és a klónodnak megvan azon tulajdonsága, hogy mindketten tudjátok mire van szüksége a családnak a hipermarketből és a piacról. Sőt, ha a klónod ment volna a piacra, ugyanúgy tudná mire van szükség onnan, mint Te. Azaz bevásárlás témában bármikor ki tudod cserélni, hogy ki hova menjen, végeredményében mindig meglesz minden dolog, amit be kellett vásárolni.
Kicsit szakmaibban megnézve. Az olyan programrészek, ahol bementként egy interfész implementáció adható meg, oda megadható legyen minden olyan interfész implementáció, ami ugyanazt az interfészt implementálja.
Azért az interfészek és az implementációk kialakításánál ügyesen fel kell azt ismerni, hogy az adott helyzetben tényleg konzekvensen kialakítható-e az interfész – implementáció kapcsolat. Miért mondom ezt?
Ha olyan interfészeket alakítunk ki, ahol a megadott metódus pl. nem minden implementációban szükséges, ott elkezdhetünk gyanakodni, hogy nem biztos, hogy a jó megoldást választottuk. Ugyanis „nyitva hagytunk egy kiskaput”, amit egy másik, esetleg figyelmetlen programozó felhasználhat, azaz egy megoldás során implementálhat, ami hibás működéshez vezethet.
Interfész szegregáció elve
Itt is interfészekről lesz szó, mint az előző elvnél, azonban is magának az interfésznek a milyenségét, felépítését fogjuk megvitatni.
De jöjjön a szokásos példa. Mindenki ismeri a közüzemli számlák világát. Kapunk minden hónapba víz, villany, gáz számlát, fizetjük a telefon, illetve kábeltévé előfizetést.
Amikor azt mondom, hogy kábeltévé előfizetés az mindenkinek ugyanazt fogja jelenteni, a különbözőség annyi, hogy mindenkinél más – más a szolgáltató. De tudjuk, hogy ebben a számlában a kábeltévéért kell fizetnünk minden hónapba.
Talán kényelmetlenebb lenne, ha a kábeltévé számlán az összes szolgáltató szerepelne, ami a piacon létezik és abból kellene nekünk kihámozni, hogy kinél is vagyunk és mennyit kell befizetnünk.
Nézzük tovább, azért az viszont elég jó, egységes megoldás, hogy bármelyik szolgáltatónál is vagyunk, végeredményébe mindegyik egy számlát fog küldeni a befizetendő összegről.
Na de nézzük, hogyan köthető ez össze az interfész szegregáció elvével. Két fontos momentum van. Az interfészekből sokkal jobb, ha több specifikus van, mint egy nagy hatalmas, ami már – már áttekinthetetlen. Ráadásul minél nagyobb egy interfész, annál biztosabb, hogy az implementáló osztályok nem fognak minden metódust implementálni, mert nincs rá szükségük. Ezzel pedig a már megtanult SOLID elvek közül kettőt is megsértünk (Egy felelősség elve, Liskov elv).
Tehát inkább legyen sok interfész, ami specifikus metódus definíciókat tartalmaz, mint egy nagy interfész, ami sok, akár egymástól kontextusban is eltérő metódus definíciót tartalmaz.
A másik lényeges dolog, hogy kódjaink ne függjenek konkrét implementációktól, kizárólag azok interfészeitől. Gondoljunk a fenti példára. A kábeltévé szolgáltatók mindegyike számlát küld a szolgáltatásról. A közlés módja (számla) ugyanaz, csak a számlakép más.
Az interfész szegregációs elvével olyan állapotú kódot hozhatunk létre, amivel egy esetleg megoldás változtatása esetén elég egy új interfész implementációt létrehozunk, tehát a már meglévő kódon nem módosítunk, csak kibővítjük (Open/Closed elv).
Dependency Inversion elv
Talán ez az egyik legbonyolultabb elv, vagy legalábbis a magyarázata eléggé nehézkes. De éljünk az előző példánkkal, azt tovább gondolva:
A kábeltévé szolgáltatók mindegyike számlát küld a szolgáltatásról. A közlés módja (számla) ugyanaz, csak a számlakép más.
Hát nagyjából ennyi, de azért ez a kódírási gyakorlatban összetettebb. Talán a legfontosabb kulcsszavai ennek az elvnek, hogy kódjaink soha ne függjenek konkrétumoktól (implementációktól), sokkal inkább absztrakcióktól.
Nagyon összefügg ez az MVC tervezési mintában tanultakkal (sőt tulajdonképpen az MVC erősen alapszik ezen az elven).
Kicsit összefoglalom: A kódjaink minden esetben tartalmaznak magasabb szintű (pl.: felhasználói felület) modulokat és alacsonyabb szintű (gépközelibb) modulokat. A vezérlési szálat tekintve ugyebár a magas szinttől haladunk az egyre alacsonyabb szint felé. Azonban a DI – nak ebben rejlik a nagyszerűsége, ezt a kötöttséget ez felrúgja, sőt mondhatnám, hogy függetleníti.
Azaz, a magasabb szintű modulok nem függnek az alacsonyabb szintű moduloktól. Ebbe belegondolva azért ez logikus is. Az, hogy a felhasználó milyen stílusú felületen írja be a nevét teljesen független attól, hogy a neve hogyan kerül lementésre az adatbázisba.
De legyünk merészebbek. A magasabb szintű modul (UI) akár le is választható az alacsonyabb szintű modulról és esetleg áthelyezhető egy másik projektbe úgy is, hogy az teljesen más alacsonyabb szintű implementációkat tartalmaz (ebben természetesen nagy jelentősége van az interfész szegregációs elvnek is).
Megpróbálom egy projekt példával szemléltetni ennek az elvnek a nagyszerűségét. Tegyük fel, hogy van egy webáruházad. A projekt konfigurációs beállításaiban MySQL adatbázis van megadva. Azonban jön egy kérés, hogy a lehető leggyorsabban át kell térni az Ügyfél kérésére PostgreSQL adatbázisra.
Miért elegendő csak egyszerűen átírni az adatbázis csatlakozási konfigurációs beállításokat? Mivel az alkalmazásunk megfelelő pontjain kizárólag adatbázis csatlakozási interfészeket adtunk meg, ezért egyszerűen csak létre kell hoznunk egy PostgreSQL nevű adatbázis csatlakozási implementációt, mely ugyanazt az interfészt implementálja, mint a MySQL implementációk. Mivel alapértelemzettként PostgreSQL adatbázist állítunk be, így futásidőben az annak megfelelő implementáció lesz érvényes.
Dependency Inversion vs. Dependency Injection
Meglepően sokszor találkozom azzal a problémával, hogy a fenti két kifejezést összekeverik, ezért ezt most rendbe tesszük, hogy mindenki számára ismert legyen a különbség.
A dependency injection (szép magyar szóval: függőség befecskendezés) egy IoC alapú (Inversion of Control) technika. Egy design pattern (tervezési minta), aminek célja, hogy egy osztály a szükséges függőségeit egy előre meghatárzott tervezési szempont alapján kapja meg.
A dependency injection az IoC elvet használja arra, hogy az osztályok soha ne legyenek felelősek a saját függőségeik (dependenciáik) kialakításáért és a függőségek élettartamáért.
A dependency inversion egy szoftvertervezési irányelv, melynek lényege, hogy alkalmazásunk egymástól függetleníthető, kicserélhető modulokból épüljenek fel. Megakadályozza, hogy az osztályok közvetlenül hivatkozzanak egymásra, ami által nem lenne függetleníthető egyik modul a másiktól.
Egy cikkben ötletesen ezeket a kifejezéseket találtam a kettő fogalom megkülönböztetésére:
- Dependency Injection: „Add meg…”
- Dependency Inversion: „Valaki gondoskodjon nekem erről valahogy…”