Korábban írtam, hogy bármilyen alkalmazást is készítünk, annak mindenképpen lesz egy architektúrája, még akkor is, ha azt nem tudatosan építjük fel. Szeretném kiemelni, hogy a nem tudatos tervezés nem pejoratív jelző, mert nem feltétlen jelenti azt, hogy ezen szoftverek rosszabbak lennének vagy nem felelhetnének meg az igényeknek. Szintén fontosnak érzem kiemelni, hogy feltételes módról beszélünk, mert egy alkalmazás sikere, jósága nem csak az architektúrától függ.
A nem tudatos tervezés előnye, hogy előbb-utóbb olyan architektúra áll elő, ami leginkább passzol a problémakörhöz és a programon dolgozó csapatok struktúrájához. A gond ott kezdődik, hogy ehhez idő kell és nagymértékben függ a csapat tapasztalatától. Egy kevésbé tapasztalt csapat esetén ez a megközelítés időkorláttal párosítva melegágya a technical debteknek és a kudarcnak.
Éppen ezért egy olyan szoftver esetén, amit több évig kell majd karbantartani, érdemes tudatosan tervezni az architektúrát. Szoftver architektúrákra is léteznek tervezési minták. Sőt, az architektúrális minták a tervezési minták egy kategóriája.
Az MVC-ről és az MVVM-ről már volt szó. Ezek a mai napig alkalmazottak és főként a UI oldalról közelítik meg az architektúrát. Azonban egy modern szoftver architektúrája nem tisztán MVC vagy MVVM. Ennek az oka az, hogy a felelősségeket szeretjük jobban szeparálni további rétegekbe további szabályokkal, így maga az MVVM és MVC komplementer egy nagyobb képben.
A nagy kép tekintetében a könyv ezen szekciójában négy fő irányvonalat nézünk meg a könyvben: a tradicionális rétegelt megközelítést, a clean architecture-t, a hexagonális architektúrát és a mikroszervizeket.
Rétegelt architektúra
A rétegelt architektúra gyökerei a szoftverfejlesztés hajnalára és a relációs adatbázisok feltalálásának idejére vezethetőek vissza. A kezdetekben a rétegelt architektúra egyetlen egy rétegből állt: az adatbázisból, mégpedig azért, mert adatok tárolására volt leginkább szükség. Az idő előrehaladtával azonban megjelentek az üzleti igények, amik valamilyen szabályok, kódok futtatását igényelték. Ezzel az SQL nyelv és implementációi lépést tartottak, de nem olyan ütemben, amit a környezet elvárt. Ennek kapcsán alakult ki az MVC, ami három rétegbe szétválasztotta a dolgokat.
Az MVC rétegezésének hátránya az, hogy az alkalmazás méretének növekedésével a kontroller egyre inkább god class lesz, ami az adatbázissal kommunikál, UI viselkedést kontrollál és üzleti logikát vezérel. Éppen ezért megjelent az igény a nagyobb átláthatóságra. Ennek egy módszere, ha további rétegeket vezetünk be és végül ez is történt.
Egy tradicionális rétegelt architektúra fontosabb rétegei:
-
Presentation layer
A presentation layer felelős a UI megjelenítésért és az interakciók lekezeléséért. Választott UI keretrendszertől függő elsősorban a felépítése, de általában vagy MVC vagy MVVM rétegezéssel kerül kialakításra.
-
Business Logic layer
A Business logic layer az alkalmazásunkban a profit termelésért felelős. Itt vannak azon számítások, szabályok, döntések megvalósításai, amelyek az alkalmazásunkat az alkalmazásunkká teszik. Például egy webshop esetén itt kap helyet a rendelés végösszege és a felhasználó hűsége alapján a számla végösszegének kiszámítása sok más egyéb mellett.
-
Data Access layer
Az adatokhoz való hozzáférést teszi lehetővé és kezeli az adatbázis kapcsolatot. CRUD (Create, Read, Update, Delete) műveleteket definiál és a fő feladata az, hogy absztrakciót biztosítson a DB és az üzleti logika közé.
-
Database
Az adatok tárolásáért felel. Akár lethet ez egy XML vagy JSON fájl is, de tradicionálisan egy relációs adatbázis, ami a konzisztenciáért és az integritásért is felelős.
A rétegelés előnye, hogy a seperation of concerns érvényesül, vagyis egy réteg csak egy valamiért felelős, így az alkalmazás karbantarthatósága növekszik. Kis alkalmazások esetén ez egy tökéletes architektúra, de megvannak a hátrányai, ami miatt manapság nem annyira népszerű.
Az egyik ilyen hátrány a tight coupling, magyarul ‘szoros függőség’ a rétegek között. Ugyan logikailag szét vannak választva a feladatok, mégis implementációban sokszor összefüggenek. Egy példa lehet a webshoppos kontextusnál maradva az, hogy hol is definiáljuk a modelleket (Pl. Felhasználó, Termék), amikkel dolgozunk? Első ránézésre logikus lenne, hogy a Data Access Layer definiálja őket, hiszen ő felel a tárolásért és a visszaállításért. Ezzel a probléma az, hogy így implicit módon minden felsőbb réteg a Data Access Layer változásainak van kiszolgáltatva. Ha megváltoztatunk egy entitást itt, akkor az a felsőbb rétegekre is hatással lesz.
A probléma áthidalására bevezethetünk absztrakciókat például interfészekkel, vagy külön rétegenkénti objektum modellekkel, amik között konvertálgatunk, de ennek is megvan a hátulütője. Ha több rétegünk van, akkor a konstans objektumok közötti konvertálgatás sebesség problémákat okozhat, illetve bármiféle absztrakció növelni fogja a karbantartás költségét.
Az eddig említett problémák a döntésekkel megoldható kategóriába esnek. Azonban a nagy hátránya ennek az architektúrának, hogy horizontálisan nehezen skálázható. A webshoppos témánál maradva képzeljük el a Black Friday esetét. Ha túl nagy a terhelés a webshoppunkon, akkor ezen architektúra mellett úgy tudjuk növelni a kiszolgálható ügyfelek számát, hogy szimplán indítunk még egy példányt az alkalmazásból és a felhasználók 50%-át az új szerverre irányítjuk. Ez két dolog miatt nem jó megoldás. Egyrészt azért nem jó, mert ha a két futó példány ugyanazon adatbázissal dolgozik, akkor az lesz előbb-utóbb a szűk keresztmetszet. Ha pedig külön-külön adatbázissal dolgozik a két példány, akkor a konzisztenciát áldozzuk be valamilyen szinten.
További hátránya ennek a fajta skálázásnak, hogy költséges megoldás. Egy teljesen új példányt kell indítanunk az alkalmazásból, aminek nagyon nem egyenletes a kihasználtsága. Ez alatt azt értem, hogy bőven többen nézelődnek, mint amennyien terméket tesznek a kosárba.
Clean architecture
A clean architecture véleményem szerint a rétegelt architektúra két fő problémáját orvosolja. Az egyik ilyen az, hogy egyértelműen definiálja, hol helyezkednek el a modellek, amikre az üzleti logika épít. A második probléma, amit megold, hogy kikényszeríti a rétegek közötti interfész szegregációt, amivel elkerülhető, hogy a rétegek között szoros függőség alakuljon ki.
Természetesen rétegek közötti interfész szegregáció bevezethető a rétegelt felépítésnél is, de ott semmi sem követeli meg.
A clean architecture Robert C. Martin nevéhez fűződik és az azonos című könyvében részletesen el is magyarázza a felépítését és a rétegelt architektúrák problémáit. Lényegében jobban összeszedve mindazt, amiről ez a fejezet szól.
Ez az architektúra a business logikát és az ahhoz kapcsolódó elemeket helyezi a középpontba, mert végső soron ez az, ami az alkalmazásunkban a bevételeket termeli. A tradicionális rétegelt architektúrának a felsorolt problémákon kívül további nagy hátulütője, hogy nem akadályozza meg azt, hogy a használt egyéb keretrendszerek keveredjenek akár az üzleti logikánkkal. Egy példa lehet webshoppos témánál maradva a számla PDF generálás. Rétegelt architektúrában ez kerülhet az Alkalmazás rétegbe is, de akár a Presentation-be is. Ha viszont az alkalmazás logika része lesz, akkor egy másik PDF generáló könyvtárra váltani minimum nehéz, ha nem lehetetlen lesz később.
A rétegek közötti szoros függőség kialakulását a S.O.L.I.D. elvek követésével és a Dependency Inversion kikényszerítésével valósítja meg. A külső rétegek a belső rétegekre függhetnek, de belső réteg nem függhet külsőbb rétegre.
A clean architecture főbb rétegei:
-
Entities
Itt kapnak helyet azon objektumok, amelyekre a business logika épít. Ezen rétegnek nem lehet semmilyen függősége.
-
Use Cases
Az üzleti logika szintje. Itt van megvalósítva mindaz, ami a fő funkcionalitáshoz kapcsolódik.
-
Interface adapters
Itt kapnak helyet mindazon adapterek, amelyek az üzleti logikánk kimenetét átalakítják a külvilág számára és a külvilágot átalakítják az üzleti logikánk számára.
-
Frameworks & Drivers
Itt kapnak helyet a külső függőségek, mint a UI és a DB.
A clean architecture rétegei az igényeknek megfelelően további rétegekre bonthatóak.
Mivel a clean architecture alapelve a rétegek elválasztása és függetlensége, ezért az alkalmazás komplexitásától függően további rétegeket vezethetünk be. Egy tipikus bontás az Entities rétegnél például a Objects és Domain Services rétegekre vágás. Az objects réteg tipikusan összetett objektumokat kezel és olyan érték objektumokat (pl. pénzösszeg, dátum-intervallum), amik önmagukban értelmezhetők, de nem rendelkeznek saját identitással.
További egyértelmű vágás lehet, hogy a Frameworks rétegbe a UI és DB-t szétvágjuk és pl a UI esetén MVVM-et vagy MVC-t alkalmazunk.
A clean architecture egy jóval több rétegre bontott variánsa az Onion architektúra, amit kifejezetten webes alkalmazásokhoz találtak ki.
Amiről eddig nem esett szó, az a skálázhatóság. Alapvetően a clean architecture-t használó alkalmazásunk 0 módosítással ugyanolyan skálázási lehetőségekkel rendelkezik, mint a rétegelt alkalmazásunk, De van mégis egy előnyünk, mégpedig az, hogy a rétegek egyértelműen elkülönülnek és ezek mentén az alkalmazásun ak későbbiek során könnyebben tovább bontható.
| Rétegelt architektúra | Clean Architectúra |
|---|---|
| Rétegek között szoros függőségek | Inversion of Control kikényszerítése |
| Nehezen izolálható üzleti lologika | Üzleti logika teljesen izolált |
| Nehezen tesztelhető DB és UI tekintetében | Üzleti logika infrastruktúra nélkül tesztelhető |
| Üzleti logika keveredhet infrastruktúrával | Infrastruktúra a külső réteg |
| Skálázhatóság nehézkes | Ideálisabb fejlődő rendszerek számára |
Hexagonális architektúra
A hexagonális architektúra és a clean architecture motivációja hasonló. A nevét a Hexagon alakú ábrázolásáról kapta, de a kitalálója, Alistair Cockburn eredetileg Ports & Adapters névvel illette, utalva arra, hogy erősen épít az adapter tervezési mintára.
Fontosnak tartom kiemelni, hogy ezen megoldás nem versenytársa a Clean architecture-nek. Hasonlóak abban a tekintetben, hogy az alkalmazás logikát helyezik középpontba, de alapvetően más üzleti „probléma” megoldására lettek kitalálva. A clean architecture komplex rendszerek modellezésére tökéletes, ahol egyértelmű réteg határokra van szükség, míg a hexagonális architektúra olyan esetekben ideális választás, ahol nagyobb flexibilitásra van szükség vagy a rendszernek számos egyéb rendszerrel kell integrálódnia.
| Clean Architecture | Hexagonal Architecture |
|---|---|
| Alapötlete az üzleti szabályok köré épülő, befelé irányuló rétegzés | Alapötlete a rendszer külső rendszerekkel való kapcsolatainak kezelése |
| Réteges, koncentrikus architektúra, fókusz az üzleti logika körül | A be- és kimeneti kommunikáció rugalmasságára és kezelése fókuszál |
| A függőségek mindig befelé, az absztraktabb rétegek felé mutatnak | Függőségek tekintetében a portokat az adapterek valósítják meg, nem fordítva |
| Nagyon strukturált, szigorú rétegleválasztás | Rugalmas, könnyen bővíthető a külső integrációkkal |
| Az üzleti logika könnyen tesztelhető elszigetelve | Az adapterek mockolhatók, a portok szabályozzák a kapcsolatokat |
| Komplex rendszerek, ahol fontos az üzleti logika tisztasága | Rendszerek, amelyek sok külső rendszerrel kommunikálnak |
Microservice architektúra
A mikroszerviz architektúra leginkább felhős/webes környezetekben népszerű, mivel minden szolgáltatás külön független, önállóan fejleszthető, telepíthető és skálázható. A szolgáltatások saját adatbázissal rendelkezhetnek és az egyes szolgáltatások különböző technológiákkal, különböző nyelveken implementálhatóak, ha szükséges.
A lényeg az, hogy ezek a szolgáltatások egymással kommunikálni tudjanak. A kommunikáció történhet HTTP felett REST endpointokkal, de a leggyakrabban egy message queue megoldást szoktak alkalmazni.
Fontos megjegyezni, hogy a mikroszerviz az inkább egy fajta tervezési elv vagy inkább egy nagyon high level nézőpont, mint egy konkrét architektúra implementáció. Ennek az oka leginkább az, hogy két különböző alkalmazást két különböző módon fogunk szolgáltatásokra bontani és a látogatószám, felhasználói viselkedés és elérhető infrastruktúra mind hatással lesz az alkalmazás architektúrájára. Az egyes szolgáltatások architektúra megvalósítása lehet rétegelt, clean architecture vagy akár hexagon is egészen addig, amíg követjük a tipikus mikroszerviz tervezési elveket:
-
Single responsibility
A SOLID elvekből ismerős elv. Egy szolgáltatás csak egy valamiért feleljen.
-
Domain-driven design
A domain-driven design (DDD) az üzleti domaint helyezi a tervezés középpontjába. A módszer alapköve az "Ubiquitous Language", egy közös nyelv, amelyet mind a fejlesztők, mind az üzleti szakértők használnak és értenek. A DDD bevezeti a "Bounded Context" fogalmát, amely a domain egy logikailag elkülönített része, saját modellel és szabályokkal. A DDD különösen alkalmas komplex üzleti logikával rendelkező rendszerek fejlesztésére. A megközelítés segít a tisztább kód létrehozásában és a jobb kommunikációban az üzleti és fejlesztői csapatok között. A DDD implementálása ugyan nagyobb kezdeti erőfeszítést igényel, de hosszú távon jelentősen javítja a rendszer karbantarthatóságát és skálázhatóságát.
-
Loose coupling
A Loose coupling (laza csatolás) azt jelenti, hogy a szolgáltatások minimális függőséggel rendelkeznek egymástól, és önállóan tudnak működni. Minden mikroszerviz független API-n keresztül kommunikál a többivel, így a belső implementációjuk szabadon változtatható anélkül, hogy az hatással lenne a többi szolgáltatásra. A laza csatolás lehetővé teszi, hogy különböző technológiákat és programozási nyelveket használjunk az egyes szolgáltatásokhoz.
-
High cohesion
A High Cohesion (magas kohézió) azt jelenti, hogy egy szolgáltatás egyetlen jól meghatározott üzleti funkcióra vagy domain területre fókuszál, és minden kapcsolódó funkcionalitást egyben tart. Minden mikroszerviz csak azokat a funkciókat és adatokat tartalmazza, amelyek szorosan kapcsolódnak az adott üzleti domain-hez, így a szolgáltatás "önmagában teljes" egységet alkot. A megfelelően kialakított kohézió esetén egy szolgáltatás módosítása nem igényli más szolgáltatások változtatását.
Melyiket érdemes választani
A szoftver architektúrában az a nehéz, hogy egy döntésről annak a meghozásának a pillanatában nem lehet egyértelműen megmondani, hogy az hosszú távon jónak vagy rossznak bizonyul. Értelemszerűen a döntés meghozásának a pillanatában a rendelkezésre álló információk alapján próbáljuk a legjobb döntést meghozni, de ezt majd csak az idő fogja igazolni. Éppen ezért, hogy melyik architektúrát érdemes választani, nem egy egyértelmű kérdés, mert nagymértékben függ attól, hogy mit kell tudnia az alkalmazásnak.
Például egy egyetemi konzolos beadandó alkalmazáshoz nem feltétlen választanék hexagon vagy clean architecture-t, mikroszervizt meg pláne nem. Ez azonban nem azt jelenti, hogy a rétegelt megoldásnál jobbra nincs is szükség sose. A választott architektúra a legtöbb esetben a megoldandó feladat, a csapat és a környezet függvénye. Lehet úgy is nekivágni egy alkalmazás fejlesztésének, hogy mikroszerviz architektúrát akarunk alkalmazni egy adott problémára, de akkor a csapatot és a környezetet úgy kell kialakítanunk.
Jelenlegi feladathoz választás
A jelenlegi URL rövidítő alkalmazás kontextusban a korábban ismertetett NFR-ek miatt a legjobb választás a hexagonális architektúra, hiszen ez egyértelműen lehetővé teszi az újrafelhasználhatóságot. Megjegyzem a clean architecture is, de amiért jelen esetben jobb választás a hexagonális megközelítés, az a méretből adódik. Viszonylag kis méretű alkalmazások esetén nem feltétlen éri meg az extra komplexitást a clean architecture.
Az alkalmazásunk három fő komponensből fog állni. A központban lesz a magja, ami az üzleti logikát fogja reprezentálni. Ennek egyik oldalán fog elhelyezkedni a HTTP API, ami a hajtó oldalt testesíti meg. A mag másik oldalán pedig az adatbázis lesz, ami a hajtott oldalt reprezentálja.
Ez az architektúra a kódban többféleképpen is megjelenhetne. Megjelenhet egy ASP.NET alkalmazásban névterekként, vagy különálló osztálykönyvtárakként. Utóbbi döntés azért jobb, mert így a termék architektúrája jobban reprezentálóik a kódban. Másik előnye ennek a megközelítésnek, hogy kevesebb lehetőséget ad a rétegek áthágására.