janos.szurovecz.hu

A régi blogom, ahova lehet írok majd még...

Repository és Finder

2013-08-04 4 perc olvasási idő

Mióta világ a világ (na jó, azért nem olyan régóta) a fejlesztők rétegekre bontják az alkalmazásokat. DDD-s fejlesztés során is így járunk el. Viszont ha nem vezetünk be szabályokat, megkötéseket, akkor könnyen olyan helyzetben találhatjuk magunkat, hogy egyes komponenseket rossz helyre teszünk, illetve egyes részek ott is elérhetővé válnak, ahol az nem lenne kívánatos.

A repository pattern gondolom mindenkinek ismerős, akinek nem, járjon utána. Sokakban felmerül a kérdés, mi a különbség a repository és a DAO között, mondván mindkettő elválaszt két réteget, mindkettő entityk kezeléséért felelős. Ez eddig igaz is, viszont van egy jelentős eltérés. Míg DAO osztályt minden entityhez létrehozunk (már ha használunk DAO-t), addig repositoryt csak az aggregate roothoz. Ugye ez azért is van így, mivel ellenkező esetben hozzáférnénk kívülről az aggregate belső elemeihez, ami ellentmond egy fontos DDD szabálynak.

Mindezek után a fejlesztő boldogan irogatja a domain réteget, a megjelenítési rétegnek szükséges metódusokat felveszi a repositorykban. Itt fel is merül a kérdés, hogy hol használjuk a repokat? Ugye ezek entityket adnak vissza, vagyis ha a interface rétegben használjuk őket, egyből elérhetővé válnak azok a üzleti logikát tartalmazó metódusok, amelyeket tranzakcióba kellene foglalnunk és amelyeket alapvetően az application rétegben használunk. Érezzük, hogy ez nem jó.

Akkor van egy másik megoldás: az alkalmazás rétegben készítsünk stateless serviceket, ahol a repok által visszaadott entityket átkonvertáljuk DTO-kra és ezt dobjuk tovább a felső rétegnek. Ez működő megoldás, viszont kicsit büdös (bad smell): ha a felületnek egy új típusú adatstruktúra kell, mondjuk egy új listaoldalhoz, akkor azt minden rétegen át kell vinnünk. Fel kell venni a repoban, a service-ben, meg kell oldani a DTO-ra konvertálást, vagyis nem túl hatékony.

Mi lenne, ha a repository osztályokat csak és kizárólag azokban az esetekben használnánk, amikor üzleti logikára van szükség? Tipikusan egy listázási funkció nem ilyen. Listázáskor csak adatokra van szükség, amiket megjelenítünk egy csinos kis táblázatban. Nem kell nekünk teljes entity, annak minden tárolt adatával együtt. Mi lenne, ha ezekhez a list és view típusú funkciókhoz valamiféle olyan osztályokat készítenénk, amelyek csak az interface (megjelenítési) rétegben vannak felhasználva, az interface ott, a megvalósítás meg mondjuk az infrastruktúra rétegben lenne? A repository osztályok összemennének, nem lennének összekeverve a business által fontos metódusok a “sima” adatlekérő metódusokkal. Ha egy új listaoldalt vezetünk be, vagy esetleg ki kell egészítenünk a riport listát egy új elemmel, akkor sem kell a domain réteghez nyúlnunk: kiegészítjük a lekérő osztályunkat, vagy akár egy újat is készíthetünk.

Ezeket az osztályokat gyakran findereknek nevezik. Pontosan olyan formában adják vissza az adatokat, ahogy arra a megjelenítési rétegben szükség van, vagyis nem kell átfuttatni az alkalmazás rétegen, nem kell konvertálgatni. Egyszerű, mint a faék. Megjegyezném, hogy a finder, csakúgy, mint a repository, nem feltétlenül egy konkrét interface, vagy osztály. Mindössze az osztály szerepét, felhasználási területét hivatott reprezentálni. Java estén pl. akár annotációként is használhatjuk, így egy osztály esetén egyből látszik annak szerepköre, valamint plusz információkkal/konfigurációval is felvértezhetjük azt.

Mit is értünk el ezzel a gondolatmenettel? Szétválasztottuk a command (módosító) és query (lekérdező) típusú kéréseket. Ez nem más, mint a CQRS.

Tekintsük a legegyszerűbb megvalósítást, ami Java esetén működik JPA-val, PHP esetén Doctrine-nal (meg még biztos sok más nyelven van rá sok más megoldás):

  • A finderekben SELECT NEW típusú lekérdezéseket hajtunk végre. Ezzel a módszerrel anélkül szétválaszthatjuk a két irányt, hogy bármit módosítanánk az adatbázisban, vagy az alkalmazásban. Javaban valahogy így néz ki.
  • Nézettáblákat (view table) használhatunk a query típusú lekérdezésekhez, amikhez elkészíthejük a read modelleket. Ezek ugyan entityk az ORM szempontjából, de nincs bennük semmi logika. Akár readOnly flaggel is elláthatjuk őket a teljesítmény növelése érdekében, feltéve, hogy az ORM támogatja. Nyilvánvalóan nem muszáj ORM-et sem használnunk.
  • Elkülönített adatbázist használunk a lekérdezésekhez, ami lehet akár relációs adatbázis, de lehet akár No-SQL vagy egyéb megoldás is, csak a képzeletünk szab határt. A lényeg, hogy a megjelenítési réteg igényeinek megfelelő formában tároljuk az adatokat, redundánsan, úgy, ahogy az a felületen megjelenik. Ebben az esetben gondoskodnunk kell arról, hogy a read modellek mögötti adathalmaz szinkronizálva legyen a write modellek mögöttivel. Ez nem feltétlenül triviális, ugye ugyanaz az adat az előbbiben többször, redundánsan is szerepelhet. Erre a problémára nyújtanak megoldás a domain eventek: commandok végrehajtása után az entitykben eventeket sütünk el, amiket elkapunk, és a frissítjük az olvasási adatbázist. PHP esetén ehhez ajánlom az általam fejlesztett predaddy-t, amiről az Intelligens eseménykezelés bejegyzésemben olvashattok bővebben.

Ha nincs rá erőforrás vagy az alkalmazás mérete nem indokolja a teljes szétválasztást adatszinten is, az első megoldást mindenképp javaslom, ugyanis viszonylag kevés plusz ráfordítással elérhető és szükség esetén bármikor tovább fejleszthető.

comments powered by Disqus