{"id":2291,"date":"2021-07-07T22:52:47","date_gmt":"2021-07-07T20:52:47","guid":{"rendered":"http:\/\/soci.hu\/blog\/?p=2291"},"modified":"2021-07-07T22:52:47","modified_gmt":"2021-07-07T20:52:47","slug":"architektura-dilemma","status":"publish","type":"post","link":"https:\/\/soci.hu\/blog\/index.php\/2021\/07\/07\/architektura-dilemma\/","title":{"rendered":"Architekt\u00fara dilemma"},"content":{"rendered":"\n<p>Tegy\u00fck fel hogy van egy klasszikus, t\u00f6bbr\u00e9teg\u0171 alkalmaz\u00e1sunk. Van benne egy service layer, ami h\u00edv valamif\u00e9le alkalmaz\u00e1s logik\u00e1t. A logika direktben hivatkozik Entity Framework DbContextre, abb\u00f3l olvas fel adatokat, m\u00f3dos\u00edtja \u0151ket, illetve direkt EF Context \u00e9s DBSet h\u00edv\u00e1sokkal menti \u0151ket.<\/p>\n\n\n\n<p>Szeretn\u00e9nk ezt az architekt\u00far\u00e1t egy kicsit jobban sz\u00e9tdarabolni, hogy \u00e1tl\u00e1that\u00f3bb \u00e9s tesztelhet\u0151bb legyen. Ez\u00e9rt el\u0151sz\u00f6r az EF adatlek\u00e9r\u0151 r\u00e9szeket kiszedj\u00fck Extract Method refactoringgal, majd ut\u00e1na \u00e1tmozgatjuk \u0151ket saj\u00e1t oszt\u00e1lyokba (Extract Class a v\u00e9ge). Ezeket az oszt\u00e1lyokat elnevezhetj\u00fck repositoryknak. Ezek ugyan nem olyan sz\u00e9p, idealiz\u00e1lt repository-k, mint amiket Martin Fowler\u00e9k \u00e1lmodtak meg, de l\u00e9nyeg, hogy az adatel\u00e9r\u0151 k\u00f3dokat szeretn\u00e9nk valamennyire elszigetelni a logik\u00e1t\u00f3l.\u00a0<\/p>\n\n\n\n<p>Mi\u00e9rt? Mert \u00edgy elszigetelve jobban meg lehet \u00e9rteni \u0151ket, k\u00f6nnyebb optimaliz\u00e1lni a benne l\u00e9v\u0151 adaterl\u00e9s\u00e9t, \u00e9s k\u00f6nnyebb az adatel\u00e9r\u00e9shez integr\u00e1ci\u00f3s teszteket \u00edrni (p\u00e9ld\u00e1ul perf okokb\u00f3l).&nbsp; A h\u00edv\u00f3, \u00fczleti logika oldalon \u00e9lesen sz\u00e9tv\u00e1lik az adatel\u00e9r\u0151 k\u00f3d \u00e9s a logika, \u00edgy az \u00fczleti logik\u00e1ba k\u00f6nnyen tudunk fake adatokat bevinni, ami addig nem volt egyszer\u0171en megoldhat\u00f3, am\u00edg az entity framework\u00f6t direktbe hivatkozva k\u00e9rt le adatokat a logika. Teh\u00e1t a c\u00e9l egy jobban kezelhet\u0151 (SOLID) \u00e9s jobban tesztelhet\u0151 k\u00f3d el\u0151\u00e1ll\u00edt\u00e1sa.<\/p>\n\n\n\n<p>A dilemma a k\u00f6vetkez\u0151. Nyilv\u00e1n egy nagy oszt\u00e1lyba nem lehet kimozgatni az \u00f6sszes helyen sz\u00e9tsz\u00f3rt eddigi adatel\u00e9r\u0151 k\u00f3dot, mert akkor kapunk egy t\u00f6bb ezer soros monstrumot.&nbsp; Valamilyen m\u00f3don sz\u00e9t kell szedni az adatel\u00e9r\u0151 met\u00f3dusokat k\u00fcl\u00f6nb\u00f6z\u0151 oszt\u00e1lyokba, ezeket h\u00edvtam az el\u0151bb repositoryknak. Ezek nem felt\u00e9tlen 1-1 entit\u00e1shoz kapcsol\u00f3d\u00f3 repositoryk, hanem lehet, hogy valami \u00fczleti ter\u00fcletet foglalnak egybe. P\u00e9ld\u00e1ul a Sz\u00e1ml\u00e1z\u00e1sRepo lehet, hogy 5-20 entit\u00e1shoz kapcsol\u00f3d\u00f3 adatel\u00e9r\u00e9st is csin\u00e1l, hisz gyakori, hogy egy teljes gr\u00e1fot kell let\u00f6lteni (mint a Domain Aggregate a DDD-ben). De nincs jelent\u0151s\u00e9ge most a rep\u00f3 felbont\u00e1s\u00e1nak.<\/p>\n\n\n\n<p>T\u00e9telezz\u00fck fel, hogy itt t\u00e9nyleg entit\u00e1s p\u00e9ld\u00e1nyok j\u00f6nnek vissza a rep\u00f3kb\u00f3l, teh\u00e1t nem rakunk be egyel\u0151re semmi mapping r\u00e9teget, amivel DTO-k vagy Domin Objecteket alak\u00edtan\u00e1nk ki. Egy ilyen refactoring sor\u00e1n nem lehet ekkor\u00e1t harapni egyszerre.<\/p>\n\n\n\n<p>Teh\u00e1t direktben entit\u00e1sokat, persistent entityket adunk vissza. Minden rep\u00f3 m\u00f6g\u00f6tt ugyanaz az EF DBContext p\u00e9ld\u00e1ny van, ez k\u00f6nny\u0171 el\u00e9rni valamilyen Dependency Injection Framework Scope-olt lifetime fogalm\u00e1val, \u00e1ltal\u00e1ban a szerviz h\u00edv\u00e1st mint scope-ot felhaszn\u00e1lva. Ez az\u00e9rt fontos, mert ha let\u00f6lt\u00f6k egy entit\u00e1st, majd let\u00f6lt\u00f6m annak egy gyerek\u00e9t, akkor bel\u00fcl az EF \u00f6sszekapcsolja a kett\u0151t, azaz a sz\u00fcl\u0151 gyerek kollekci\u00f3j\u00e1ba beleker\u00fcl a gyerek, a gyerek sz\u00fcl\u0151 referenci\u00e1ja pedig a sz\u00fcl\u0151re fog mutatni. Ez a k\u00f6z\u00f6s Context egy kritikus koncepci\u00f3 az eddigi k\u00f3dban, ugyanis b\u00e1r lehet nem tudatos azokban, akik \u00edrt\u00e1k a k\u00f3dot, de az EF Context r\u00e9v\u00e9n implicit kapcsolatba ker\u00fcl sok sornyi adatel\u00e9r\u0151 k\u00f3d, \u00e9s csak az\u00e9rt m\u0171k\u00f6d\u00f6tt helyesen az adatel\u00e9r\u00e9s, mert az EF \u00edgy van meg\u00edrva.<\/p>\n\n\n\n<p>A get m\u0171veletek sz\u00e9pen \u00e9s eleg\u00e1nsan becsomagolhat\u00f3k rep\u00f3 m\u0171veletekbe. Ha van pl. egy GetInvoicesByCustomer met\u00f3dus, akkor az visszaad Invoice entit\u00e1sokat, ami m\u00f6g\u00e9 tetsz\u0151leges szintig bet\u00f6lt\u00fcnk minden egy\u00e9b gyermek vagy sz\u00fcl\u0151 objektumokat. Igaz, ez a r\u00e9szlet nem l\u00e1tszik a met\u00f3dus szignat\u00far\u00e1b\u00f3l, de ezzel egy\u00fctt \u00e9l\u00fcnk. A logik\u00e1t k\u00f6nny\u0171 tesztelni fake adatokkal, hisz csak ki kell cser\u00e9lni a teljes rep\u00f3 met\u00f3dust valami fix adatot visszaad\u00f3 stubbal.<\/p>\n\n\n\n<p>A m\u00f3dos\u00edt\u00e1sok rep\u00f3k fel\u00e9 t\u00f6rt\u00e9n\u0151 kommunik\u00e1ci\u00f3ja m\u00e1r nehezebb. Az insert m\u00e9g elmegy, hisz pl. az InvoceRepository.AddInvoice vagy AddInvoceWithDetails vagy hasonl\u00f3 le\u00edrja, mit v\u00e1runk a m\u0171velett\u0151l. A h\u00e1tt\u00e9rben egyszer\u0171en h\u00edvunk egy DbSet.Add-ot.<\/p>\n\n\n\n<p>A Delete hasonl\u00f3an egyszer\u0171, ha top level objektumr\u00f3l van sz\u00f3, DeleteInvoice(Invoice).<\/p>\n\n\n\n<p>Az eg\u00e9sz viszont sokkal macer\u00e1sabb\u00e1 kezd v\u00e1lni, amikor p\u00e9ld\u00e1ul egy sz\u00fcl\u0151 objektum al\u00e1 kell berakni egy gyerek objektumot.<\/p>\n\n\n\n<p>P\u00e9ld\u00e1ul, a rep\u00f3b\u00f3l lek\u00e9rek egy sz\u00e1ml\u00e1t, majd ehhez kellene t\u00e9teleket hozz\u00e1adni.<\/p>\n\n\n\n<p>Az InvoiceItems kollekci\u00f3 a visszakapott Invoice entit\u00e1s r\u00e9sze. Ha ehhez egyszer\u0171en hozz\u00e1adunk egy \u00faj InvoceItemet, akkor az EF err\u0151l tudni fog, hisz neki v\u00e9gig van referenci\u00e1ja a gy\u00f6k\u00e9r Invoice objektumra, \u00edgy a SaveChanges csak \u00fagy el fogja menteni a gyerek sort, be\u00e1ll\u00edtva az FK-j\u00e1t a sz\u00fcl\u0151 Invoice-ra.<\/p>\n\n\n\n<p>A gondom az, hogy ez teljesen implicit. Nem tudok \u00e9rtelmes met\u00f3dust felrakni a rep\u00f3ra, ami kommunik\u00e1ln\u00e1 a logika azon sz\u00e1nd\u00e9k\u00e1t, hogy \u0151 itt most egy gyerek objektumot akar hozz\u00e1adni a sz\u00fcl\u0151h\u00f6z. Emiatt tesztelni se tudom rendesen.<\/p>\n\n\n\n<p>Ugyanez a probl\u00e9ma a m\u00f3dos\u00edt\u00e1sokkal. Lehozok a rep\u00f3.GetValami met\u00f3dussal egy entit\u00e1st. Ezt direktben m\u00f3dos\u00edthatja a logika, majd a SaveChanges-re az EF csak \u00fagy tudja, hogy erre update SQL parancsot kell kiadni.<\/p>\n\n\n\n<p>Megint, nem foghat\u00f3 meg j\u00f3l tesztb\u0151l, hogy a logika sz\u00e1nd\u00e9ka az volt, hogy m\u00f3dos\u00edt egy entit\u00e1st.<\/p>\n\n\n\n<p>Teh\u00e1t, az \u00e1talak\u00edtott k\u00f3d \u00fagy n\u00e9z ki, hogy bet\u00f6lt mindenf\u00e9le adatot mindenf\u00e9le repo.Get h\u00edv\u00e1sokkal. Ezek nem elszigetelt adatok, hanem \u201c\u00f6ssze vannak ragadva\u201d. Az EF mindet k\u00f6veti. A logika tetsz\u0151leges m\u00f3don megbabr\u00e1lja ezt a t\u00f6bb darabb\u00f3l \u00f6sszerakott gr\u00e1fot, majd az EF ezt elmenti \u201cokosba\u201d. De r\u00e9m ut\u00e1lom az ilyen okosba megold\u00e1sokat.<\/p>\n\n\n\n<p>Hogyan lehet erre tesztet \u00edrni, ami azt vizsg\u00e1lja meg, hogy a logika a lehozott Invoce-hoz hozz\u00e1adott egy \u00faj t\u00e9telt? Hogy lehet megfogni tesztb\u0151l, hogy milyen entit\u00e1s milyen property-j\u00e9t mire \u00e1ll\u00edtotta be a logika? Azt felt\u00e9telezem, hogy fake rep\u00f3k vannak a logika m\u00f6g\u00f6tt. Ahogy megbesz\u00e9lt\u00fck, a Getekben adatb\u00e1zis helyett k\u00f6nny\u0171 kamu adatokat behazudni mem\u00f3ri\u00e1b\u00f3l, azaz visszaadhatunk teljesen felt\u00f6lt\u00f6tt gr\u00e1fokat. P\u00e9ld\u00e1ul visszaj\u00f6n egy Invoice, benne k\u00e9t InvoiceItem, \u00e9s referencia egy Customerre. Az logika legyen az, hogy ha a Customer neve Mari, akkor berak egy \u00faj t\u00e9telt, InvoiceItemet, \u201cTalp \u00e9s egy\u00e9b massz\u00e1zs\u201d tartalommal.<\/p>\n\n\n\n<p>A logika egyszer\u0171en hozz\u00e1ad egy new InvoiceItemet a Getben visszakapott Invoice objektumhoz, majd a rep\u00f3 m\u00f6g\u00f6tti EF DbContext.SaveChanges csak \u00fagy el fogja menteni.<\/p>\n\n\n\n<p>Hogyan lehet tesztb\u0151l detekt\u00e1lni, hogy a logika t\u00e9nyleg hozz\u00e1adta a sz\u00e1mlat\u00e9telt?<\/p>\n\n\n\n<p>Azt lehet tenni, hogy elt\u00e9r\u00edtj\u00fck a SaveChanges h\u00edv\u00e1st (mock scenario), \u00e9s abban \u00e1tn\u00e9zz\u00fck a mit? Itt nincs igazi EF DbContext, nincs semmi state a kez\u00fcnkben, amit meg vizsg\u00e1lni.<\/p>\n\n\n\n<p>Ha a rep\u00f3n lenne valami explicit met\u00f3dus, akkor k\u00f6nnyebben lehetne tesztelni. Ha p\u00e9ld\u00e1ul lenne egy InvoceRepo.AddInvoiceItem(Invoice, InvoiceItem) met\u00f3dus, akkor ezen kereszt\u00fcl explicit l\u00e1tszik a logika sz\u00e1nd\u00e9ka, hogy egy gyerekelemet akkor hozz\u00e1adni. Ez k\u00f6nnyen tesztelhet\u0151.<\/p>\n\n\n\n<p>De mit csin\u00e1ljon az igazi AddInvoiceItem implement\u00e1ci\u00f3? Adja \u0151 hozz\u00e1 az itemet az \u00e1tpasszolt Invoice-hoz? Ok, megteheti. De az EF kialak\u00edt\u00e1sa miatt a logika ezt minden tov\u00e1bbi n\u00e9lk\u00fcl megteheti a visszakapott Invoice entit\u00e1son is, mindenf\u00e9le rep\u00f3 h\u00edv\u00e1s n\u00e9lk\u00fcl, m\u00e9gis m\u0171k\u00f6dni fog az igazi implement\u00e1ci\u00f3, m\u00edg a teszt hib\u00e1t jelezne.<\/p>\n\n\n\n<p>A probl\u00e9ma f\u0151 oka szerintem az, hogy a k\u00e9t r\u00e9teg k\u00f6z\u00f6tt csak \u00fagy \u00e1tmennek az entit\u00e1sok, \u00edgy a logic layerben t\u00f6rt\u00e9nt m\u00f3dos\u00edt\u00e1sok csak \u00fagy \u00e1tl\u00e1tszanak a rep\u00f3 (data access) r\u00e9tegbe.<\/p>\n\n\n\n<p>Ez nyilv\u00e1nval\u00f3an egy nem eleg\u00e1ns, nem el\u00e9g tiszta modell.&nbsp;<\/p>\n\n\n\n<p>De t\u00e9rj\u00fcnk vissza az elej\u00e9re. Van egy spagetti k\u00f3dunk, amiben egym\u00e1s ut\u00e1n vannak logika \u00e9s adatel\u00e9r\u0151 k\u00f3dok. Ez akarjuk elkezdeni kis kock\u00e1zat\u00fa refactoringokkal darabolni tesztel\u00e9s \u00e9s kezelhet\u0151s\u00e9g miatt. Mivel az eddigi k\u00f3d er\u0151sen \u00e9p\u00edtett az EF-re mint change trackerre, ezt nem lehet egyszer\u0171 refactoringgal kiszedni bel\u0151le. Az \u00e1tl\u00e1that\u00f3bb szerkezet \u00e9rdek\u00e9ben kirakjuk saj\u00e1t oszt\u00e1lyokba, rep\u00f3kba, de tudjuk, hogy er\u0151sen kil\u00e1tszik az EF a rep\u00f3 m\u00f6g\u00fcl (google: Leaky Abstraction). Ezt tudjuk, elfogadjuk, mert nem lehet egy nagyon sok sorb\u00f3l \u00e1ll\u00f3 k\u00f3dot csak \u00fagy \u00e1talak\u00edtani. A k\u00e9rd\u00e9s teh\u00e1t az, hogyan lehet ilyen k\u00f6t\u00f6tts\u00e9gek mellett \u00fagy kialak\u00edtani egy \u00e9rtelmes rep\u00f3 interf\u00e9szt \u00e9s \u00e9rtelmes egy\u00fcttm\u0171k\u00f6d\u00e9st a k\u00e9t r\u00e9teg k\u00f6z\u00f6tt, ami elfogadhat\u00f3 szint\u0171 logika tesztel\u00e9st tesz lehet\u0151v\u00e9?<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Tegy\u00fck fel hogy van egy klasszikus, t\u00f6bbr\u00e9teg\u0171 alkalmaz\u00e1sunk. Van benne egy service layer, ami h\u00edv valamif\u00e9le alkalmaz\u00e1s logik\u00e1t. A logika direktben hivatkozik&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[25,49,74,4,82,42,89],"tags":[],"class_list":["post-2291","post","type-post","status-publish","format-standard","hentry","category-adonet","category-architektura","category-entity-framework","category-szakmai-elet","category-test-driven-development","category-testing","category-unit-test"],"_links":{"self":[{"href":"https:\/\/soci.hu\/blog\/index.php\/wp-json\/wp\/v2\/posts\/2291","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/soci.hu\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/soci.hu\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/soci.hu\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/soci.hu\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=2291"}],"version-history":[{"count":1,"href":"https:\/\/soci.hu\/blog\/index.php\/wp-json\/wp\/v2\/posts\/2291\/revisions"}],"predecessor-version":[{"id":2292,"href":"https:\/\/soci.hu\/blog\/index.php\/wp-json\/wp\/v2\/posts\/2291\/revisions\/2292"}],"wp:attachment":[{"href":"https:\/\/soci.hu\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=2291"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/soci.hu\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=2291"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/soci.hu\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=2291"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}