Vertical slice architektúra az ASP.NET-ben

2021.08.19. · 8 perc olvasás

Az ASP.NET-projektek strukturálásának egy modern, napjainkban egyre nagyobb teret nyerő praktikájáról lesz szó az alábbiakban. A cikk bevallottan nem nyújt semmi forradalmit, és ha a kedves olvasó beszél angolul, akkor rögvest el is kezdheti nézni ezt a konferenciaelőadást, mely átfogóbban mutatja be a témát, mint ami a jelen rövid cikk terjedelmébe belefért.

Fontos rögvest megemlíteni, hogy más architekturális és programtervezési mintákhoz hasonlóan az alábbiakban bemutatott minta sem egy csodaszer; nyilvánvalóan nem fog egycsapásra megoldani minden problémát, nem vált ki minden más mintát, és biztosan léteznek olyan projektek, amelyeknél e minta követése több hátránnyal járna, mint előnnyel.

Legacy projektstrukturálás

Az ASP.NET-megoldásokban klasszikusan alkalmazott strukturálás az N-tier architektúrát követi: a projektekre osztás nagyjából az adatbázis, adathozzáférés, üzleti logika és UI/Web minta szerint történik, melyek egymásra épülő rétegeket alkotnak.

Ebben a struktúrában jellemzően létezik egy Models mappa valahol, amely az Entity Framework által mappelt entitásokat tartalmazza anemikus formában; mindössze adatokkal, viselkedés nélkül. Az üzleti logikát jellemzően entitásalapú Service komponensekre osztva hozzuk létre, mely tipikusan az adathozzáférési réteg szintén entitásalapú Repository komponenseit veszi igénybe az adatok lekérdezéséhez. Ehhez társul egy ugyancsak entitásalapú kontroller a UI/Web rétegben, amely a rendszer által nyújtott műveleteket teszi elérhetővé a kliensek számára.

Példának okáért, ha van egy Order entitásunk, akkor ehhez készül egy OrderService, mely lényegében tartalmazza az Order entitáson végzett összes művelethez szükséges összes metódust. Ez az OrderRepository segítségével kérdezi le az adatokat az adatbázisból, a műveletet a legfelsőbb szinten pedig az OrderController kezdeményezi.

Ha az üzleti logika komplexitása megkívánja, akkor az entitásalapú Service réteg fölé tipikusan készül egy újabb réteg, például OrderFacade, mely az OrderService (és esetleg további Service komponensek) több metódusát hívja meg magasabb szintű műveletek végrehajtásához.

Mindemellett gyakori általános jellemzője a strukturálás e tradicionális módjának, hogy a komponenseket a technikai szerepük szerinti mappákban helyezzük el a projekteken belül: Models, Controllers, Validators, DTOs, Enums, Services és így tovább.

A fentebb bemutatott megközelítésnek van egy erős jellegzetessége, amelyet fontos felismernünk: A rendszer által végzett műveletekhez szükséges üzleti logikát szinte teljes egészében általános használatú komponensekben helyezi el, melyekből horizontális, egymás feletti rétegeket alkot.

A mindennapos munka komplexitása horizontális rendszerben

Ha módosítunk kell a megrendelésekhez kapcsolódó műveletek egyikét, akkor ehhez hozzá kell nyúlnunk az OrderService komponens egy vagy több metódusához a BusinessLogic projektben. Aztán át kell ugranunk a Models projektbe, ahol szintén el kell végezni a módosítás releváns részét az Order entitáson (tulajdonságok hozzáadása, törlése, módosítása) . Könnyen meglehet, hogy az adathozzáférési projektben is el kell végezni néhány módosítást az OrderRepository komponens lekérdezésein. Majd meg kell keresnünk a vonatkozó ViewModel-t vagy DTO-t egy ugyancsak jellemzően távoli mappában vagy projektben, és azt is módosítani kell. Az utánahúzandó mappelési profil is egy sokadik különálló helyen lehet, és szélsőséges esetben valahol szintén teljesen máshol található egy Enums mappa is, amelyben az entitás által használt enumokat kellhet módosítanunk. Végül rejtőzhet egy Validators mappánk is valahol, ahol meg kell keresnünk és frissítenünk kell a vonatkozó validátort.

Egyetlen művelet kontextusában akartunk végrehajtani egy módosítást, mégis hozzá kellett nyúlnunk a rendszer számos részéhez; projektről projektre, mappáról mappára ugrálva.

Egy komplex rendszerben lényegében nincs garanciánk arra nézve, hogy az összes szükséges módosítást elvégeztük a rendszer különböző részein, mely könnyen hibákhoz vezethet. Ez a projekthez újonnan csatlakozó fejlesztőket sújtja a legnagyobb mértékben, hiszen gyakran hosszú idő megtanulniuk, hogy mi mindenre kell figyelni – milyen egymástól távoli, abstrakt nevű mappákban elhelyezett komponenseket szükséges módosítani – a rendszerrel való mindennapos munka során.

Azt sem tudhatjuk biztosan, hogy a módosításainknak milyen hatása lesz a rendszer más komponenseinek és műveleteinek működésére, hiszen sok minden más is hivatkozhat az általunk módosított általános használatú metódusokra, főként a * Service* komponensek esetében. Ez folyamatosan rendkívüli körültekintést igényel a rendszerrel való munka során; mindig résen kell lennünk, ellenőriznünk kell a metódusokra hivatkozó más kódrészeket, nehogy a módosításunk mellékhatásokkal vagy regresszióval járjon (melyeket ugye jó esetben a unit vagy magasabb szintű tesztek kiszűrnek).

Sok esetben azonban az figyelhető meg, hogy a Service és Repository komponenseink tele vannak olyan metódusokkal, amelyek egyetlen egy helyről, egyetlen művelet kontextusában vannak meghívva. Ez esetben sem nyújt hozzáadott értéket ez a strukturálási paradigma; épp ellenkezőleg: megnehezíti a megoldás átlátását és navigálását azáltal, hogy szétszórja a leggyakoribb munkakontextus – a művelet – különféle részeit.

„Big ball of mud”

A fenti, valóságtól nem is elrugaszkodott leírásból a big ball of mud antipattern kezd kirajzolódni.

Nem lehet jól érvelni a rendszer működésével kapcsolatban, mert az egy-egy művelethez szükséges üzleti logika általános használatú komponensekben van szétszórva, amelyek több ezer sorosra is nőhetnek, és egymástól logikailag nagyrészt független, alacsony kohéziójú metódusokat tartalmaznak. Ezek a metódusok egy komplexebb rendszerben keresztül-kasul vannak meghívva más komponensekből, és gyakran nehezen állapítható meg, hogy pontosan mi szükséges egy új funkcióhoz vagy egy meglévő módosításához.

Lényegében eljutunk ahhoz a felismeréshez, hogy az egyik legismertebb szoftvertervezési elv, a kód újrahasználása (* DRY*), könnyedén spagettikódhoz vezethet, ha kényszeresen alkalmazzuk, mert sok esetben a formailag hasonló/azonos kódrészleteknek valójában más oka van a változásra.

A vertikalizálás koncepciója

A vertikalizálás egy egyszerű elv, és éppen ezért vonzó: lényegében a rendszert alkotó használati esetek önálló, magas kohéziójú egységekbe való rendezését jelenti.

Ha van egy „megrendelés elfogadása” használati eset a rendszerünkben, akkor arra létre lehet hozni egy külön osztályt, például AcceptOrderCommand, mely az egyes megrendelések elfogadásához szükséges logikát a lehető legteljesebb módon enkapszulálja. Ezt el lehet helyezni egy hasonló nevű mappában (UseCases/AcceptOrder), ahol praktikusan elhelyezhetők a művelethez specifikusan kapcsolódó kérés- és válaszmodellek, állandók, enumok, és minden más.

Ha nem várható, hogy az adatperzisztálási mechanizmust meg kell változtatni a későbbiekben, akkor az adatlekérdezési logikát is el lehet helyezni a vertikális szeletben, közvetlenül a DbContext-tel dolgozva, a Repository réteg kihagyásával. Ezt azonban érdemes alaposan átgondolni, mert egy adatelérési absztrakciónak lehetnek előnyei vertikális paradigmában is, és ha sok vertikális szeletben lenne megismételve ugyanaz a komplex adatlekérdezés, akkor azt nyilvánvalóan hasznos kivonni valamiféle központi komponensbe.

A vertikalizálás magas szintű rugalmasságot biztosít az egyes használati esetek implementálásában, hiszen a szeletek önállóan, közvetlenül elvégezhetik a szükséges műveleteket, anélkül hogy kényszeresen közös használatú komponensektől kellene függeniük. Így az egyik művelet történhet az Entity Framework ORM-funkcionalitását igénybe véve, egy másik művelet történhet nyers SQL-utasítással vagy tárolt eljárás meghívásával, és így tovább.

Fontos azonban megjegyezni, hogy továbbra is létezhetnek közös használatú komponensek, és egy komplex rendszerben biztosan szükség is lesz rájuk. A lényeg az eltérő megközelítés: először a vertikális szeletbe kerül minden, procedurális egyszerűséggel. Ezután, ha a komplexitás megkívánja, a code smelleket megfigyelve lehet refaktorálni, metódusokba és osztályokba kivonni. A tényleges újrahasználati igény esetleges megjelenésekor (de nem előbb) pedig ki lehet szervezni a logikát egy központi helyre.

A kódduplikáció és a refaktorálás vertikális architektúrában való kezelésével kapcsolatban ezt az angol nyelvű cikket érdemes elolvasni.

Járulékos előnyei is megfigyelhetők a vertikális architektúrának. Több más modern szoftverarchitekturális elvvel és paradigmával jó párosítást alkot:

Vertikalizálás a MediatR-rel

A vertical slice architektúra implementálásának legismertebb konkrét formája a MediatR nevű Nuget csomag használata, mely a közismert AutoMapper szerzőjétől származik.

Ennél a megoldásnál IRequestHandler implementációt tudunk létrehozni az egyes kérésmodellekhez, és ezek alkotják a vertikális szeleteinket.

Az általános, cross-cutting concern jellegű funkcionalitás implementálásához pedig IPipelineBehavior interfészt kínál, melynek konkrét implementációi az összes request handlernél végrehajtódnak, így kiválóan alkalmas loggolásra, teljesítménymérésre, validációra.

A MediatR egy kis komplexitású megoldás, melyet könnyű használatba venni, így érdemes kipróbálni vele a vertikális architektúrát legalább egy proof of concept projekt erejéig.

Zárásként mindössze némi kritikát fűznék ahhoz, ahogyan a MediatR-t gyakran emlegetni szokták fejlesztői körökben:

Egy kritikusabb hangvételű informatív angol cikk a témában: No, MediatR Didn’t Run Over My Dog.

A vertical slice architektúra használata az ASH Szoftverháznál

A .NET-csapatban bevallottan még csak most kacsingatunk a MediatR felé egy új projekt tervezési fázisában. A műveletek enkapszulálásának koncepcióját azonban sikeresen implementálta a .NET-csapat egy futó projekt keretében, olyan formában, hogy a legtöbb kontrollervégpont egyetlen vonatkozó Operation osztálypéldány-metódust hív meg, mely fogadja a kérésmodellt, elvégzi a műveletet, majd visszaadja a válaszmodellt; tipikus Service réteg nélkül.

További információk és ajánlások

Angolul tudó ASP.NET-fejlesztőknek ajánljuk a következő előadást és vonatkozó GitHub-mintaprojektet, mely a Clean Architecture-rel kombinálja a MediatR-alapú vertikalizálást. Ez a minta egyre elterjedtebbé válik ASP.NET-fejlesztői körökben, így hasznos megismerkedni vele:

Clean Architecture with ASP.NET Core 3.0 • Jason Taylor

Kérdése van? Lépjen velünk kapcsolatba!

1117 Budapest Budafoki út 97.
+36 1 353 9790
info@ashszoftverhaz.hu
Bejárat a Prielle Kornélia utca felől, térképen jelölve található.