Patrik Votoček

weBlog



Service vrstva Doctrine (1/2)


Když jsem se rozhodoval , jakou knihovnu použít pro modelovou část Nelly, byla jednou z možností Doctrine ORM. Původně jsem si Doctrine ORM 2 nevybral a začal jsem psát vlastní ORM, postavené nad dibi. Po přibližně měsíční práci jsem dospěl do fáze, kdy to nějak fungovalo, ale čekala mě hromada další práce, jako například optimalizace, celkový refaktoring a hlavně otestování v praxi. V tu dobu jsem své předchozí stanovisko nepoužít Doctrine znovu přehodnotil a začal ji používat.

V těchto dnech tomu bude rok, co používám Doctrine. Je to výborná knihovna a autoři na ní odvedli velký kus práce. Nicméně ani Doctrine není kompletní řešení modelové vrstvy aplikace. Schází jí totiž poslední „kostička do skládačky“. Tou je modelová Service vrstva.

Tato chybějící vrstva je věc, která mi nedává spát. Snažím se totiž nalézt ideální řešení a zkouším co se dá. Dospěl jsem do bodu, kdy prostě nevím, jak dál a tak se chci poradit. V zásadě jsem dospěl ke dvěma možným řešením a nemůžu se rozhodnout, které z nich zvolit, či zda jít úplně jinou cestou.

Doctrine mimo jiné implementuje repository pattern. To znamená, že načítání dat z databáze má následující životní cyklus.

  1. repository chci článek číslo 1
  2. repository se ptá IM máš u sebe článek číslo 1?
  3. UoW odpovídá nemám
  4. repository se říká mapperu načti mi článek číslo 1
  5. maper načte článek číslo 1 a pošle ho repository
  6. repository ho uloží do IM
  7. repository mi vrátí článek číslo 1

Jinak řečeno máme specializovanou část modelu, která se stará o načítání dat a optimalizaci načítání. Tj pokud budu chtít podruhé načíst článek číslo 1. repository ho už načte z paměti a nebude sahat do databáze.

S tím souvisí implementace service vrstvy. Dá se totiž podle toho oddělit její odpovědnost.

U obou následujících variant je instance service třídy to jediné, k čemu má Presenter / Controler přístup.

Varianta „manželé“

V této variantě se service třída stará pouze o úpravy entit. O načítání se stará repository. Ukázka řekne více než 1000 slov:

$service = new ArticleService(...);
$article = $service->repository->find(1);
$articles = $service->repository->findByCategory($category);
$articles = $service->repository->findAll($offset, $limit, $order);

Načítání dat se provádí vždy pomocí přímého dotazu do repository. Service třída se tedy dělí o práci s repository.

Shrnutí

  • – pro většinu entit existují 3 třídy (entita, service a repository)
  • + jasné rozdělení, o co se kdo stará

Varianta „superman“

V této variantě se naopak service třída stará o vše a Presenter / Controler by se k repository vůbec neměl dostat. Opět ukázka:

$service = new ArticleService(...);
$article = $service->find(1);
$articles = $service->findByCategory($category);
$articles = $service->findAll($offset, $limit, $order);

Service třída vládne všemu a stává se z ní tak „super“ třída.

Shrnutí

  • + pro většinu tříd existují 2 třídy (entita a service)
  • – „super“ třída, která má nastarosti vše

Kterou z těchto variant zvolit a proč?

14 komentářů


Hrach

nový

Co znamená prosím „stará pouze o úpravy entit“, doctrine nepouzivam, nevim co si pod tim mam predstavit, co to vlastne dela. Jinak z toho co sem pochopil bych se klonil k supermanovi.

Hrach avatar

Patrik Votoček

nový

[1] zjednodušeně řečeno INSERT, UPDATE, DELETE kdežto repository se stará o SELECT.

Patrik Votoček avatar

Ondřej Mirtes

nový

Jen tě trochu poopravím – Unit of Work pattern má na starosti zjišťování a flushování všech provedených změn do databáze. To „kešování“ entit do paměti, abych měl v aplikaci pouze jednu instanci objektu s daným PK, zajišťuje pattern Identity Map. Ten je v Doctrine implementován jako pole ve třídě UoW.

Ondřej Mirtes avatar

Patrik Votoček

nový

[3] jasně upsal jsem se (už mě z toho trochu hrabe). V článku jsem to opravil.

Patrik Votoček avatar

Jan Tichý

nový

Varianta Superman je svazující, zbytečně se matlá do jedné třídy víc věcí, které se často hodí vyměňovat či po částech reusovat.

Pokud Ti pomáhá DI kontejner, tak dokonce i varianta manželé je zbytečná, protože se v ní často zbytečně instancují věci, které pak v daném dotazu vůbec nevyužiješ.

Takže skoro nejlepší je využívat jen a pouze přímo $repository->findWhatever() nebo $service->doWhatever() podle toho, co zrovna potřebuješ.

BTW jak vytváříš úplně novou instanci Article? Nejspíš přes operátor new Article(). Dříve či později dospěješ ještě k potřebě továrničky na nové instance.

Takže nakonec pak budeš možná třeba u jedné entity pracovat se čtveřicí Factory – Repository – Entity – Service (či víc různých services). A jak říkám – je zbytečné tohle všechno balit do jedné Service (ať už jako manžele, nebo jako supermana) a je nejlepší to nechat zcela rozdělené a používat z toho jen to, co zrovna opravdu potřebuješ…

Jan Tichý avatar

Patrik Votoček

nový

[5]

Takže skoro nejlepší je využívat jen a pouze přímo $repository->findWhatever() nebo $service->doWhatever() podle toho, co zrovna potřebuješ.

chápu to dobře že i repository se registruje jako služba do DI kontejneru (a nevyužívá se tak $em->getRepository('App\Model\FooEntity'))?

BTW jak vytváříš úplně novou instanci Article? Nejspíš přes operátor new Article(). Dříve či později dospěješ ještě k potřebě továrničky na nové instance.

O to se mě stará Service viz: https://github.com/…/Service.php#L86

Jinak o napojení service na DI kontejner mám napsaný další článek (pokračování).

Patrik Votoček avatar

Jan Tichý

nový

„chápu to dobře že i repository se registruje jako služba do DI kontejneru“

Ano, což neznamená, že DI kontejner nemůže při jejím vydávání volat $em->getRepository(‚Ap­p\Model\FooEn­tity‘)

„O to se mě stará Service…“

Přijde mi příliš svazující mít servisy pojaté takhle úzce ve smyslu „každá entita má právě jednu svoji servisu“ respektive mít prostě obecně servisu, která pracuje s něčím jako $this->entityClass.

Svazuje Tě to v možnosti mít pro jednu entitu víc různých servis či naopak mít servisu pracující s více entitami najednou. V důsledku to svádí k drsnému porušování Single Responsibility, k narvání všech operací souvisejících s danou entitou do jedné třídy, v důsledku pak k omezené reusability.

Pokud je to ale jenom doplňková záležitost pro nejběžnější případy, „aby se to psalo rychlejc“, a vedle toho používáš vhodně jiné „typy servis“, tak OK.

Jan Tichý avatar

pave.kucera

nový

@Jan Tichý: mohl by jsi prosím uvést nějaký příklad, kdy se hodí mít víc servis pro jednu entitu?

pave.kucera avatar

Jan Tichý

nový

@pave.kucera: Mohl bys uvést nějaký příklad, kdy se to nehodí? :)

Jan Tichý avatar

pave.kucera

nový

@Jan Tichý: nemohl – věřím, že se to tak může vyplatit, ale v kontextu webů, které tvořím, mě nenapadá žádná situace, kdy bych po takovém řešení sáhl. Proto se ptám, ať si rozšířím obzory :).

pave.kucera avatar

Patrik Votoček

nový

Snažím se už pár dní najít případ kdy se to hodí a zatím marně (možná sem hodně ovlivněn mím stávajícím řešením).

Můžeš prosím uvést nějaký ten příklad?

Patrik Votoček avatar

Jan Tichý

nový

Tak třeba už jenom uživatel/identita. Co všechno se s ním vedle základního CRUDu může dělat!

Třeba provádět před uložením různé validace, které mohou být závislé třeba na konkrétní aplikaci nebo situaci nebo kontextu.

Třeba mergovat s jinými uživateli – často s další návazností na spoustu dalších částí aplikace, takže je dobré mít merger jako speciální třídu, do které si třeba ostatní části aplikace například registrují své listenery.

Autentizace – jeden uživatel/identita se může přihlašovat přes dvacet různých způsobů, od loginu přes facebook až po bůchvíco. Každý autentikátor je ve své podstatě samostatná servisa.

A co teprve když již existujícího uživatele s loginem budu chtít přicvaknout ještě navíc k FB autentizaci. Nebo stávající uživatel zapomene heslo a budu mu chtít poslat nový token. Opět se pro tohle všechno hodí mít relativně samostatné třídy.

Autorizace – opět je to nějaká servisa pracující (mimo jiné) s uživatelem/i­dentitou na svém vstupu.

Tohle všechno se dá přece hezky elegantně udělat řadou malých vyměnitelných třídiček. A ne to rvát všechno do nějaké jedné zbastlené megatřídy ala Nette User ;).

A ony i samy třídy obsluhující základní CRUD se mohou hodit občas vyměnit, aniž bych musel zasahovat do entity, například pokud budu chtít dělat nějakou transparentní vícejazyčnost nebo transparentní verzování…

Jan Tichý avatar

maryo

nový

Vim ze je to pul roku starej text, ale v cem je lepsi rvat repository do service containeru? Resp. v cem je lepsi mit $this->get(‚wtfRepo­sitory‘) nez $this->getDoctrine()->getRepository(‚wtf‘) ?

maryo avatar

Přidej komentář



cenzuruje váš poskytovatel připojení?

Kategorie

Čtu

Kamarádi