Pět vrstev modelu
Dneska bych chtěl shrnout svůj dosavadní myšlenkový posun ohledně podoby a struktury modelu aplikace, což je téma, které jsem nakopl ve svém předchozím článku a otevřel i na dubnové Poslední sobotě.
Není to asi nakonec nic objevného a zejména z pohledu lidí pohybujících se v jiných jazycích je to určitě vynalezení kola. Ale k některým věcem se prostě člověk musí dopracovat sám. A navíc v oblasti PHP nejsou přístupy založené na DDD vůbec známé, tak to třeba aspoň pár PHP vývojářů nakopne k přemýšlení.
Starý špatný Active Record
Přístup pomocí Active Recordu zjednodušeně řečeno mapuje strukturu a data z databáze do objektové reprezentace v aplikaci tak, že:
- co tabulka, to jedna třída,
- co řádek tabulky, to jedna instance,
- všechny gettery, settery a obslužné metody jsou zpravidla v jedné třídě.
Obecná struktura takového modelu se pak dá znázornit následujícím obrázkem:
Práce s modelem založeným jen na databázi a Active Recordu pak vypadá například takto:
$article = new Article(123);
$article->setStatus(Article::STATUS_PUBLISHED);
$article->setPublished(new DateTime);
$article->save();
Active Record je poměrně jednoduchý, kompaktní a pochopitelný. Pokud s ním ale budete pracovat déle, budete mít větší aplikaci, větší tým programátorů, budete chtít různé části modelu sdílet napříč různými aplikacemi, pokaždé ale s nějakou drobnou odchylkou, tak narazíte na spoustu překážek:
- Jeden record je právě jedna tabulka – pokud vám jedna entita překrývá více tabulek, které chcete skrýt, nebude vám Active Record stačit.
- Silná vazba na strukturu databáze – veškeré atributy nebo vazby mezi tabulkami se zrcadlově přenášejí do aplikace, to se dost často nehodí. Typicky v případě jiných typů uložišť, které s relačními databázemi nemají vůbec nic společného.
- Není to model – v lepším případě je to jen spodní část modelu, více viz Model není pouze databáze
- Více různých uložišť – Active Record přestává stačit v okamžiku, kdy chcete v rámci jedné entity používat více uložišť, ať už více různých databází, nebo například nějakou memcache jako alternativu k primární databázi.
- Rozšiřitelnost, reusability, modularita – skoro jakákoliv odchylka či změna se musí dělat děděním, což je z principu špatně. Výměnu funkčnosti či uložiště nemůžete provést snadným nahrazením jedné třídy za jinou. Jakákoliv změna zasahuje přímo do samotné třídy entity, což je špatně.
- Single responsibility – drsně se porušuje princip single responsibility, jedna třída je tu zodpovědná za úplně všechno.
- Testovatelnost – kompaktní třída, která dělá uvnitř sebe úplně vše, se velmi špatně testuje, nedá se využívat mockování apod.
Obecná struktura modelu
Active Record je prostě příliš úzký a omezující. Zkusme si jej tedy dekomponovat do více vrstev podle jednotlivých odpovědností:
Berte to primárně jako obecný pohled na věc. Ve své aplikaci nakonec nemusíte mít nutně všech pět vrstev, některé můžete vynechat nebo se vám mohou slít dohromady. Je ale dobré je tam všechny vidět a rozlišovat. Ostatně, i samotný Active Record je pak jen speciálním případem tohoto obecného pojetí – bez service vrstvy a s nevhodně sloučenou entitou, repository a mapperem.
Pojďme si teď popsat jednotlivé vrstvy a jejich odpovědnosti.
Entita
Oproti Active Recordu jsme z entity vyházeli všechny obslužné metody, všechno načítání, ukládání, mazání. Zůstala nám tedy jen přepravka na data, která nabízí pouze své gettery a settery:
$article = new Article;
$article->setTitle('Lorem ipsum');
$article->setStatus(Article::STATUS_PUBLISHED);
$article->setPublished(new DateTime);
echo $article->getTitle();
Navíc je fajn, pokud se můžeme u entity v jakémkoliv okamžiku spolehnout alespoň na základní typové konverze, takže ať pomocí funkce setPublished() uložíme cokoliv, tak se nám nikdy nestane, že by nám pak getPublished() vrátila cokoliv jiného, než instanci DateTime či NULL.
Entita pak slouží jako přepravka, ve které se předávají data z mapperu přes repository až ven z modelu, v ideálním případě na konci ještě přes nějakou servisu. Nic víc, nic méně.
EDIT: Z diskuze pod článkem a navazujících odkazů vyplývá, že entity v podobě pouhých přepravek bez jakékoliv další doménové logiky nejsou vhodným přístupem. V tomto našem případě by tak i sám Article mohl mít navíc definovanou metodu publish(), která by například nastavovala datum a status. Srovnejte se Service níže.
Mapper
Mapper zajišťuje práci s konkrétním uložištěm. Typicky tedy načítání, ukládání či mazání článku. Tímto konkrétním uložištěm pak může být databáze, memcache, filesystém nebo třeba i nějaká vzdálená služba.
$mapper = new ArticleDbMapper;
$article = $mapper->find(123); // vrací entitu Article
$mapper->save($article);
$mapper->delete($article);
Jeden mapper je pro daný typ entity odpovědný za veškerou obsluhu jednoho typu uložiště. Pokud budeme mít například tři různé mappery ArticleDbMapper, ArticleMemcacheMapper a ArticleFileMapper, všechny budou navenek nabízet totéž, jenom si to každý uvnitř sebe pořeší podle svého.
Když budu potřebovat v průběhu aplikace začít ukládat články někam úplně jinam, napíšu si pro to další mapper, aniž bych musel sáhnout do entity Article nebo do kteréhokoliv z dosavadních mapperů.
Repository
Byla by ale chyba, pokud bychom se přímo v controlleru aplikace rozhodovali, do kterého uložiště chceme článek uložit, kdybychom v controlleru vytvářeli instanci nějakého konkrétního mapperu. Pokud bychom se totiž například později rozhodli, že chceme místo databáze ukládat články do souborů, museli bychom to přepsat na mnoha místech aplikace.
Proto se mezi controller a mapper vkládá ještě jedna vrstva – repository, která nás odstiňuje od konkrétního použitého mapperu. V controlleru pak nikdy nevoláme přímo konkrétní mapper, ale vždy repository:
$articleRepository = new ArticleRepository;
$article = $articleRepository->find(123);
$articleRepository->save($article);
$articleRepository->delete($article);
Repository v nejjednodušším případě funguje jako mechanický předavač povelů. Nabízí navenek stejné funkce, jako mappery, akorát se až teprve sám uvnitř sebe rozhoduje, kterému mapperu povel předá. Změna konkrétního mapperu pak může být otázkou přepsání jedné proměnné uvnitř repository.
Takové vyčlenění samostatné repository nám pak ale umožňuje dělat mnohem zajímavější věci. Například snadno mohu před databázi předsunout cachování do memcache. Z aplikace pak i nadále volám stále tytéž funkce repository, v mapperech také nic neměním, jenom uvnitř samotné repository přepíšu kód tak, že se pokouším inteligentně kombinovat ArticleDbMapper a ArticleMemcacheMapper.
Umožňuje nám to dokonce i takové věci, jako vracet z metody find() pokaždé instance různých tříd, například v závislosti na hodnotě nějakého databázového sloupce či rozšiřující tabulky:
// vrátí Television (potomek Product)
$product = $productRepository->find(5);
// vrátí Notebook (potomek Product)
$product = $productRepository->find(123);
EDIT: Jak v diskuzi níže poznamenal Aleš Roubíček, název Repository je chybný, ve skutečnosti se jedná o DAO.
Service
S využitím popsaných vrstev už si většinou vystačíme a takový model je dostatečně volný a flexibilní. V controllerech pracujeme s instancemi entit, voláme nad nimi jejich gettery a settery. A pokud něco potřebujeme načíst, uložit nebo smazat, požádáme o to příslušnou repository.
Stále nám tu ale přetrvává dost silná závislost rozhraní modelu na jednotlivých proměnných entity, na jejím vnitřním uspořádání. Například takové publikování článku:
if ($article->getStatus() != Article::STATUS_DRAFT) {
throw new Exception('Lze publikovat jen draft článku.');
}
$article->setStatus(Article::STATUS_PUBLISHED);
$article->setPublished(new DateTime);
$articleRepository->save($article);
Často se hodí tohle celé do něčeho zabalit, například:
$post->publish();
A k tomu právě slouží nejvyšší service vrstva. Servisy jsou vlastně fasády postavené nad různými entitami. Jedno volání servisy v sobě zpravidla balí více volání nad entitou, může dokonce pracovat i nad více entitami, odstiňuje nás od jednotlivých sloupců, od jednotlivých getterů a setterů.
Možné využití fasád je nepoměrně větší. Můžeme například článek nabízet jednou v podobě AdminArticle servisy, která umožňuje veškerou editaci, a jednou jako UserArticle servisy, která nám zpřístupní pouze funkce na čtení. A podobně.
Ucelený příklad
Konkrétní podoba celého modelu, na které doceníte celou obecnost popsaných vrstev, pak může vypadat například takto:
Ale to je hrozně moc psaní!
Na první pohled to může vypadat, že pro jednoduché ukládání a načítání článku budu muset vytvořit deset tříd a napsat stovky či tisíce řádků kódu. Tak to ale vůbec nemusí být. Množství nutného kódu vůbec nesouvisí s architekturou modelu, jsou to dvě zcela různé nezávislé problémy.
Pokud si i normálně píšete vše ručně, tak tady je to skoro pořád ten samý kód, co byste bušili kdykoliv jindy, jenom místo cpaní do jedné třídy ho máte logicky rozdělený do tří či více tříd.
Ale dovedu si představit, že pro to lze napsat i systém typu Doctrine nebo Ormion. V něm jediné, co bude potřeba pro typické případy, je definovat nějaký YAML či jiný definiční soubor, a všechny entity, repository i mappery se z něj postaví samy. Rozdíl oproti stávajícím péhápéčkovským ORM systémům ale bude v oproštění se od omezujícího ActiveRecordu a nabídnutí výrazně vyšší flexibility právě ve smyslu důsledného rozdělení do popsaných vrstev.
Dokonce už mám v tomhle směru i něco málo nastřeleného, ale to zase až příště. Teď jenom chci, aby bylo jasné, že tenhle článek není o tom, jestli se bude psát hodně či málo kódu. Je to o myšlenkovém zbavení se ActiveRecordu a nastřelení lepšího rozvrstvení a obecné struktury, která mě nesvazuje a se kterou se dá pracovat.
Prezentace na závěr
Pro zájemce přikládám i prezentaci, kterou jsem k tématu promítal na Poslední sobotě.
Co si o tématu myslíte? Budu vděčný za jakékoliv vaše připomínky!
Související odkazy:




Z toho jsem pochopil, že Repository může implementovat úplně stejné rozhraní jako Mapper a je vpodstatě jen speciálním případem Mapperu.
Umísťovat ukládání mimo entity je sice dobře co do SRP a má to určité logické zdůvodnění, ale přijde mi to spíše jako peklo. Představa, že po přidání nového sloupce v databázi budu upravovat nejen entitu, ale i Mapper, mi nepřijde zrovna lákavá. Navíc opomenutí této úpravy se projeví celkem nepříjemně pouze chybným chováním, nikoli chybovou hláškou. (Jasně, TDD pomáhá.)
Principiální problém je spíše v tom, že přidání nového sloupce (nebo spíše nové vlastnosti entity) není zpětně kompatibilní změna.
Navíc může být problém, pokud entita dává k dispozici pouze informace o stavu (například isXXX()) a ne stav samotný (typicky getXXX) aby umožnila některé budoucí změny.
[1] „Zpětná kompatibilita při vkládání nového sloupce v databázi…“
Asi jsem to špatně pochopil, mohl bys to prosím popsat trochu více? Jak dojde k nekompatibilitě, pokud přidám do databáze nový sloupec, o kterém aplikace ještě neví?
[1] Rozhraní mapperu a repository se může na styčných bodech překrývat a třeba v ukázce v článku tomu tak je, ale obecně to tak být vůbec nemusí. Tohle je jenom specifický případ, ukročení z obecnosti. Navíc je to jen implementační detail. Rozhodně to ale v žádném případě není principiální vlastnost repository a mapperu.
Jestli musím po přidání nového sloupce upravovat i mapper, to záleží na tom, jak to mám implementované. Ale není to vždy nutné, jde to udělat i tak, aby se musela upravovat jen definice entity. Opět za cenu ústupků z obecnosti. Více v dalším pokračování článku.
Přidání nového sloupce do databáze bez současné aktualizace mapperu je většinou zpětně kompatibilní, pokud tam v databázi nejsou nějaké restrikce bez defaultních hodnot. A pokud to přeci jenom zařve, tak je to naprosto v pořádku – pak je to chyba v deploymentu, nikoliv v návrhu modelu.
Přidání nového sloupce do mapperu bez aktualizace databáze (například u sdílených knihoven) zpětně kompatibilní není, ale to je opět naprosto v pořádku, to ani nemá být. Problémy opět signalizují špatně nastavený deployment.
Poslední poznámku o rozdílu mezi isXXX a getXXX jsem nepochopil.
Není lepší mít v té entitní vrstvě ty tři jednořádkové metody save, find a delete? Klidně to mohou posílat jen dál, ale ušetří to práci v tom, že zvenku se nemusím o repository starat. Což je možná i definice vrsty, že je vždycky vidět jen jedna nižší.
[4] Tohle jsem řešil ve speciálním článku na toto téma, viz Obslužné metody modelu i včetně všech navazujících komentářů.
Moc pěkně srozumitelně popsáno. Díky!
Sám teraz objavujem a skúmam DDD, takže napíšem veci, ktoré ja vidím inak, ako námet na diskusiu.
V tomto prípade by som teda doménovú logiku presunul do metódy Article::publish a servisná vrstva by iba načítala entitu Article z DB, vykonala metódu a entitu znova uložila. Controllery by o žiadnom repository podľa mňa ani vedieť nemali. O všetky konkrétne use case-y by sa mala starať servisná vrstva.
Pozn.: Viem si predstaviť, že úlohu servisnej vrstvy by prebrali controllery, ale potom by som tam už nepridával aj tú servisnú vrstvu.
[1] Pro ty, co stejně jako já nikdy neviděli zkratku SRP, tak je to pravděpodobně Single responsibility principle: http://en.wikipedia.org/…ty_principle
Jak by byla implementovana treba metoda $article->getComments() ? Volala by servisu nebo repository ktera by natahla komentare? Kterou? Nejak si to se svou spatnou fantazii nedokazu predstavit.
Tento typ návrhu mi určitě zjednoduší změnu úložiště, ale kladu si otázku, jak často tento problém v reálu řeším. Ve většině případů je to tak, že potřebuji doplnit položku, která jde napříč celou aplikací. To ovšem v tomto případě znamená, že místo jedné třídy musím měnit tři další a roste riziko chyby. Zdrojem problému „potřebuji tam doplnit ještě jedno políčko“ je většinou zákazník.
Ahoj, mám jen takový dotaz: Než jste podobné věci začali ve vaší firmě řešit, zkusili jste se podívat na to, jak se to řeší jinde? Nejen v PHP, ale i v jiných tehcnologiích? Říká vám něco JPA? Java? Mě jde jen o to, že z mého pohledu vynalézáte znovu kolo (jako PHP programátoři mají ostatně ve zvyku, že, rovněž nesouhlasím s tvým tvrzení že to prostě někteří lidé MUSÍ sami vynalézt). Tak když už tak rádi znovu vynalézáte existující kola, tak prosím (myslím to s vámi dobře), se zkuste inspirovat nějakou existující, prověřenou a funkční technologií, než začnete něco bastlit a pak zjistíte – a hele, ono to ale za takových a takových podmínek nikdy nebude fungovat!
Nevidím a upřímně nechápu jediný důvod co vás k tomu vede (ani ne tak ke psaní „vlastního“ ORM, jako spíše k prezentování vašeho „rádoby“ objevu) – krajina ve které hledáte poklady, je dávno ZCELA a kompletně prozkoumána.
Abych nemluvil jen „teoreticky“ a bez „důkazů“, právě například zmíněné JPA pro Javu je konečné, úplné a kompletní řešení ORM. Je to specifikace, která je otevřená a má mnoho implementací. Každopádně ohledně JPA bylo formálně dokázáno že v oblasti ORM již nelze jít dál. Neboli, že neexistuje žádné databázové schéma, či ORM related princip, který by JAP nepokrývalo.
Pokud chcete víc informací, tka třeba zde: http://www.google.com/search?…
A prosím nechte si stranou řeči typu: je to moc složité a já tomu nerozumím. JPA je kompletní řešení. Neexistuje a je dokázáno že nemůže existovat něco, co neřeší.
A pokud je to pro vaši firmu moc „velká ryba“, a musíte si prostě apriori napsat vlastní ORM, tak si prosím napřed nastudujte JPA, nebo alespoň jeho podmnožinu, kterou chcete ve svém ORM řešit, než začnete znovu vynalézat kolo.
Rovněž argumentace typu: Ono je to ale jen pro Javu, je zcela lichá. Jedna z implementací JPA – jmenuje se Hibernate, má svou portaci do PHP (přiznávám, že nepříliš zdařenou), tato portace se jmenuje Doctrine…
Ruku na srdce pánové prozkoumali jste alespoň jiné již existující free, či open ORM řešení pro PHP?
Dále např diskuze zde: http://stackoverflow.com/…-orm-library …
Kolik z výše jmenovaných jste prozkoumali? O kolika vůbec víte že existují?
Dle mého názoru je dobré se rozhlížet, než začnu implementovat „své, to zaručeně nejlepší řešení“…
A další věc, neodsuzujte dobré produkty jednoduše proto, že „nejsou moje“, nebo proto, protože je neumíte používat…
Tento článek mě bohužel pouze dále utvrzuje v mém přesvědčení že PHP je ztracený jazyk, který je určen pro lidi, kteří chtějí prostě jen bastlit nějaké webíky a ne vytvářet něco, co jde nazvat slovem software.
Když už vytváříte ORM, říkají vám něco related DesignPatterny? DAO? DataMapper? ActiveRecord? Table/RowDataGateway? IdentityMap? Nebo z nich znáte jen ActiveRecord a možná DataMapper?
Nevnímejte prosím můj příspěvek jen jako kritiku, ale trochu (dobrá, asi trochu víc) se orientuji i v jiných technologiích než jen PHP (JavaEE, Ruby, lehce .NET) a musím říci, že PHP je prostě doménou rádoby programátorů, kteří si raději dělají všechno sami, než aby se rozhlédli a inspirovali ve svém okolí…
Jestli „PHP guru“ píše o tom, jak „objevil“ ORM, jakoby to byla kdovíjaká novinka… – nezlobte se na mne, ale jsem z toho jenom smutný. Já nekritizuji snahu, naopak, ta je vítaná, jen mi je líto, že se tolik energie směřuje zcela špatným směrem…
Zareaguj prosím Honzo na můj příspěvek, zajímá mě můj názor a tvé odpovědi na mé otázky…
Vida to se bude dobre pamatovat, mnemotechnicka je „SERMU“ (Service, Entita, Repository, Mapper, Uloziste)
Ale vazneji: zajimavy rozbor.
V praxi to vetsinou vypada tak, ze je stanoveno „dame to na mySQL“, v lepsim pripade „pouzijem dibi“ (nebo jiny DB layer).
Malokdo premysli z pohledu „co je mozne delat se clankem“ vetsinou to je „co delat se clankem v DB“. Snad tenhle clanek prispeje k jinemu pohledu na praci s daty.
Pěkný článek. Rád bych doplnil moji vlastní zkušenost.
Myslím si, že většina lidí je postižená MySQL, jinou databází, nebo ORM.
Aniž si to uvědomujeme, naše programování se točí kolem databáze, nebo ORM a nemáme čertsvou hlavu, protože datanázi používáme od našeho prvního pokusu s PHP doteď.
Ryba v moři taky nezná nic jiného, než moře a tak mi prostě budete muset věřit a ověřit si co tu píšu sami.
Všem doporučuju si založit nějaký malý projekt v Ruby (žádné Railsy, čisté Ruby) a věnovat se mu aspoň půl roku. Ukládejte si objekty třeba do pole, které místo MySQL dotazů sekvenčně prohledáváte (toto je důležité).
Výsledkem bude, že se vám z hlavy vykouří zažité sr.čky, otevře se hlava a budete čerství a schopni poznat, jakým způsobem se ubírat a jakým ne.
Až se uvolní databázové postižení, budete schopni dále pracovat jako doposud, ale vaše myšlení nebude tak postižené a budete dělat lepší rozhodnutí.
Vaše programování by se nemělo omezovat na to, jak to převedete do databáze.
(přesvědčete se sami, jestli to tak je)
2Washo
$article->getComments()
budeš volat jenom po volání
$article = $articleRepository->find(123);
Takže veškerá data už bude uvnitř modelu a method getComments() vrátí datu kterou potřebuješ.
[9] Protože jsem asi dva dny před vyjitím tohoto článku znovu-objevil kolo, řešil jsem stejnou otázku: zatím mě napadlo řešení, když komentáře jsou naplněny v entitě už z mapperu. Výhoda je, že entity jsou schopny fungovat zcela bez mapperů (můžu ručně vytvořit instance, pospojovat, otestovat,…). Ztrácí se ovšem lazy-loading a načítá se zbytečně víc věcí, než je třeba. Jsem docela zvědavý na další článek/y – doufám, že přinesou odpověď i na tuto otázku=)
Pre php existuje nieco podobne pristupom ORM (Java programatori urcite poznaju Hibernate) a vola sa to Propel http://www.propelorm.org/ Doporucujem pozriet, nastudovat a pouzivat :))
[15] Myslim ze lazy-loading je nutny. Ono to taky muze vypadat treba: $article->getComments()->getFirst()->getAuthor()->getContact()->getEmail() … asi je to blby priklad ale pokud uz bych mel nejaky ORM nasadit, chci abych ho mohl pouzivat takto a bez lazy-loadingu by to asi nebylo mozne.
Abych jenom nekritizoval, dari se mi docela dobre mapovat databazi (pri cteni) na objekty pomoci Nette\Objectu… akorat jsem si dodelal cachovani properties a udelal si implementaci Collection. (Zajimave by bylo tento pristup nasroubovat na tuhle 5ti vrstvou terorii.)
Ono teda… nejdriv jsem rozdelil praci s ulozistem na 2 pristupy. Cteni a manipulaci s daty. Hrozne mi to pomohlo.
Clanok je naozaj pekny, myslienky v nom naozaj chytre (BTW pozrite si quick start tutorial k Zend Frameworku), ale je to zase jeden z clankov, kde je modelova situacia „trosku“ mimo reality.
Tak ako uz niekto pisal vyssie, zaujima ma predsa len realnesie nasadenie, kde mame tony vazieb 1:N a nemanej M:N. Mohol by autor v dasom poste popisat implementaciu „situacie“:
Kazdy post moze byt v M kategoriach. Kazda kategoria moze mat N postov. Kazdy post ma X komentarov.
Z pohladu Conrolera nas zaujima:
Ziskaj, text postu, zoznam kategorii, komentare (citanie postu).
Ziskaj zoznam postov a ku kazdemu pocet komentarov (archiv kategorie).
atd.
K tomu nejaky pokec, kde co nakesovat.
Prosim neberte ma ako lamu, co si pyta hotovy kod (sam som doteraz zapasil s podobnou situaciou s pomocou AR v Yii frameworku), ale ak sa ma cz/sk PHP komunita posunut dalej, treba ju trocha „poedukovat“.
THX.
Podobně jako Brano ([7]) jsem začal trošinku koukat po DDD a tak mám na věc stejný pohled jako on (a mám v hlavě článek na toto téma).
Hlavně souhlasím s tím, že Repository by neměla být viditelná z Controlleru. Jediné, co by měl Controller vidět, je Service a Entities. Service a Entities tak vlastně tvoří interface Modelu. To odpovídá tomu Tvému obrázku vrstev.
Pokud nemáš moc doménové logiky (typicky jen CRUD), tak se Ti Services zdrcnou na pouhé delegování volání na Repository. Tomu se říká „macaroni code“ a doménovým entitám se říká, že jsou anemické (de facto jsou degradovány na pouhé přenašeče dat – Data Transfer Objects). DDD puristé tvrdí, že je to špatně, ale pokud opravdu v aplikaci žádná doménová logika není, tak tam fakt není co dát. Pak je IMHO dobré přemýšlet o tom, jestli je DDD to pravé ořechové pro tento projekt. Já jsem pro krátkém přemýšlení došel k tomu, že je DDD stále použitelné, ale je vhodné si ho nějak „přiohnout“.
Osobně jsem pro možnost 2). Kód Services můžeme nagenerovat a v runtime je jedno volání navíc úplný prd.
Dále bych dodal k Mapperu, že je to jen volitelná pomoc pro Repository. Jinými slovy – Repository může používat pro přístup k datům nějaký Mapper, ale klidně tam můžeme přímo tlačit SQL (nebo komunikovat přes REST s nějakým cool NoSQL úložištěm
.
Kešování – je možné dělat na všech vrstvách dle potřeby. Mapper, Repository, Services, aplikace (třeba celá stránka).
Lazy loading – ten by neměl probublat za hranici Service, tzn. rozhraní Service by mělo být navrženo tak, aby dalo v DTO všechna potřebná data.
srigi: Jak jsi popsal, co nás zajímá z pohledu Controlleru, tak tím jsi de facto definoval interface Modelu, takže interface Service.
Service bude mít tedy metody GetWholePost(..), GetArchivedPosts() atd.
Entity Post, Comment a Category budou úplně normální třídy napsané klasicky podle zásad OOP. Žádná vazba na cokoliv, jen čistý kód (položka Id reprezentující unikátní klíč je ok).
Repository může mít klidně výše zmiňované „lazy-load umožňující“ univerzální znovupoužitelný rozhraní. Pokud chceme opravdu krutopřísnou rychlost, pak bude mít ale pravděpodobně Repository stejný interface jako Service.
V Repository pak už přímo přistupuješ k úložišti. Je jedno, jestli přímo přes SQL nebo nějaký Mapper.
Článek jako obvykle na úrovni.
Několik poznámek:
Teorie entit, mapperů a ůložišť je zde dovedena k dokonalosti – jak už to tak u Honzy s teorií bývá :)
Celá tahle problematika je však jenom jedním z problémů, které člověk musí řešit.
Pokud například veškerý zmíněný kód nechám vygenerovat nějakým nástrojem, je v podstatě jedno jestli se vše bude nacházet v jedné třídě nebo pěti.
Hlavním problémem je rozumné načítání dat především (ale nejen) z relačních databází. To co David Grudl nazval „předbíháním budoucnosti“.
V šystému, který jsem vytvořil a v současnosti ho celý přepisuji (haha) jsem tohle implementoval takhle:
Volání attach() vede k modifikaci vnitřního stavu objektu $articles a v důsledku k použití jiného SQL dotazu pro „load“ článků.
Jakub Vrána něco takového připravuje pod názvem, který si nepamatuji (viděl jsem pouze zmatené video, kde Jakuba neustále překřikoval jakýsi člověk ve čtverečkované kovbojské košili).
Jestli jsem to dobře pochopil Jakub se ve svém řešení obejde bez toho druhého volání attach(). „Potřebnost“ dat detekuje až ve chvíli jejich použití a k „doloadování“ potřebných dat používá mechanismus založený na SQL operátoru IN.
Co z toho vyplývá? Otázky nastíněné v článku je třeba brát vážně, avšak existuje mnoho dalších problémů z nichž asi nejvýraznější je efektivní získávání dat.
Když problém attach projektuju do .NET světa, tak tam se to řeší třeba v Linq2Sql, resp. Entity Frameworku, obdobně – jen se ta metoda jmenuje Include. Lazy-loading (i na DTO (POCO) objektech) tam je také.
Je to záležitost Mapperu, resp. interní problém Repository, protože ten ukázaný kód patří do Repository. Jak jsem ale psal, za hranice Services lazy-loading IMHO nepatří a entity lezoucí ze Services by měly být čisté DTOs.
Ale pravda, je to jen jeden z mnoha implementačních detailů, který je třeba v praxi řešit…
[16] Propel jsem podrobně nezkoumal, ale z článku na Zdrojáku o ORM a jejich homepage mi přijde, že jde o implementaci Active Recordu…nebo se mýlím?
Ahoj, je to trochu offtopic, ale koukal jsi na Doctrine 2? Co si o něm myslíš?
Pokud třeba chci na nějakém view zobrazit článek a jeho komentáře tak by se to dalo řešit i tak, že použiji
$article = $articleService->find(123);
$comments = $commentsService->findByArticle($article);
připadne takhle by to mohla volat rovnou article service a sestavit článek přímo.
Co se přítomnosti servis tříd týče tak se určitě hodí ve chvíli kdy chci řešit třeba řízení přístupu, validování atd.
co se týče konstrukce $article->getComments()->getFirst()->getAuthor()->getContact()->getEmail() tak takhle by to opravdu nemělo vypadat pokud všechny ty funkce nevracejí stejný datový typ, protože by to odporovalo [LoD | http://en.wikipedia.org/…w_of_Demeter
[11] Tomáši, vůbec netuším, na co ve svém příspěvku reaguješ. Přijde mi, že sis pro sebe vykonstruoval řadu mylných předpokladů, o kterých se v článku nepíše ani slovo a o kterých článek ani vůbec není (vymýšlíte si na zelené louce to, co už je vymyšleno; právě jste objevili orm; vůbec jste se nepodívali, jak se to řeší jinde; prezentujete tyhle známé myšlenky jako vlastní nápad; preferujete vlastní řešení před hotovým cizím; bastlíte si tam nějaké vlastní orm), a pak s nimi polemizuješ…
[11] Tome, tenhle článek neni o ORM. Jestli to chápu přesně, tak ORM může být vrstva konkrétního DataMapperu, který pracuje s relační databází.
Naopak mám radost, že to někdo v PHP komunitě konečně pěkně shrnul a vysvětlil v češtině.
Hele děcka, ale to co tu nazýváte Repository, vůbec Repository není. Je to Data Access Object. Repository pouze abstrahuje datový zdroj za něco jako kolekci (pro čtení). Slouží pouze pro dotazování datového zdroje, metody Save a Delete tam rozhodně nepatří! viz. http://martinfowler.com/…ository.html
[11] Tomáši, měl bys sis asi příště přečíst víc než jen nadpis, než se tak pěkně rozohníš :) Obávám se, že Honza má několikanásobně větší přehled v teorii objektového návrhu než ty.
A když už jsme té kritiky: už někdy před půl rokem jsem Vám psal, že máte na webu objektove.cz pěkný blábol, když píšete, že „Značná část jejich aplikací postrádá business logiku.“, a stále to tam bije do očí.
Aleši, v tom odkazovaném článku se píše:
Objects can be added to and removed from the Repository, as they can from a simple collection of objects, and the mapping code encapsulated by the Repository will carry out the appropriate operations behind the scenes.
Z čehož mi vyplývá, že ukládání a mazání do Repository patří.
Kam to patří podle Tebe?
[28] Môžeš dať k dispozícii odkaz, kde to tak riešia? Fowler píše, že do Repository môžeš pridávať aj z neho mazať, viď [30].
Ešte prikladám odkaz na podrobnejší popis dobrej architektúry podobnej tej v článku: http://jeffreypalermo.com/…ture-part-1/
Pro pochopení je možná lepší nakreslit Entitu mimo ten seznam vrstev, jako to mají třeba tady: http://biese.files.wordpress.com/…ring2-01.JPG V odkazovaném obrázku ale postrádám vrstvu, která by rozhodovala o tom, jaké se použije DAO (u nás Mapper).
[28] To by se pak tedy vlastně asi sloučili vrstvy Repository a Mapper do jedné DAO, přičemž DAO by mohla být víceúrovňová, že?
Jako třeba ArticleDAO, která by měla ty metody find, save, delete, atd. a vnitřně by mohla volat třídu ArticleMySQLDAO nebo ArticleOracleDAO apod. To mi zní celkem rozumně :) Nevydá to na samostatný článek, Aleši?
[30] ale ne pomocí metod Save a Delete, to pak není rozhranní kolekce (Add, Remove). Navíc jejich (těch metod) název už není Persistent Agnostic. Odpověď na poslední otázku najdeš v Unit of Work. Každopádně, pokud používáš některý ze zaběhnutých ORM, všechny tyto vzory jsou v nich již implementovány. IQueryable je ekvivalentem Repository, DataContext/Session zase Unit of Work, DataMapper je zde taky, Identitní mapa by také neměla chybět atd.
[23] Propel je kombinacia ORM a ActiveRecord. Umoznuje v jednej Entite pristup k viacerim tabulkam, lazy-loading a kaskadovane ukladanie objektov. Jedine co ma asi spolocne s ActiveRecord je to ze na ulozenie objektu nepotrebujete specialnu service alebo dao, staci vam samotny objekt. Co je na nom super (a ma to spolocne napr. s Java ORM – Hibernate) je to ze umoznuje vytvarat jak selecty rovno nad objektami, tak pustat primo sql selecty nad databazi. Mensi nevyhodou je konfigurace, no od toho sa da odhliadnut, kedze vam potom Propel vsetky Modely a Query objekty vygeneruje sam…
[33]Asi chápu. Takže pokud dělám přístup k databází přímo přes ADO.NET (SQL commandy), tak bych měl mít rozhraní pro čtení (Repository/) a rozhraní pro zápis (UnitOfWork).
UnitOfWork by mělo fungovat tak, že si bude jen poznamenávat požadované změny a když se zavolá Commit, tak teprvé vytvoří db kontext (třeba SqlConnection a otevře //Transaction) a změny se skutečně provedou.
To pak vede na to, že mám specializovanou Repository (ArticleRepository) i UnitOfWork (ArticleUnitOfWork). Tak?
[3] Mohl bys (treba v dalsim clanku) uvest nejaky priklad? Nejak mi to neni jasne.
K isXXX() vs. getXXX(): Da se to ukazat na viditelnosti – isPublic(), isPrivate(), isVisibleFrom(…) apod. umozni v budoucnu pridat novou viditelnost.
[8] Ano.
Komentář smazán autorem blogu pro přílišnou míru zhůvěřilosti.
[35] Unit Of Work ti stačí jeden na všechno, není třeba specializovaný. viz třeba http://msdn.microsoft.com/…d882510.aspx
Díky za ten odkaz, zajímavý počtení. Ale na konci se stejně píše You may also use a Unit of Work implementation that forces you to do reads and queries through the Unit of Work to make state change tracking easier. :)
Jinak článek (a hlavně komentáře od Aleše) mě zase pošouply o kousek dál. Jestli to chápu správně, tak UnitOfWork i Repository typicky používá vespod ten samý DAO (typicky nějaký ORM).
K dané problematice mě napadá několik dotazů/postřehů:
Dovolím si odpovědět:
Odstavec 1: Injectovat Repository do Entity se mi moc nelíbí a radši bych to nedělal.
Odstavec 2: Jak už naznačil Aleš (a už to mám i z jiných zdrojů), Repository by měla řešit jen read-only záležitosti. Pro úpravu/mazání slouží UnitOfWork. A ta pracuje tak, že si pamatuje, co bylo vytvořeno/upraveno/vymazáno a až v okamžiku volání Commit() tyto operace skutečně provede. Takže UnitOfWork je ten, kdo se stará o mergování při pokusu o update jedné entity.
Odstavec 3: Jak píšu, toto řeší UnitOfWork – pošle „nakešované“ změny nejednou.
Odstavec 5: Nejsem si jist, jestli jsem to správně pochopil, ale tohle IMHO řeší Dependency Injection.
Odstavec 6: Prezentace řeší jen prezentaci, tedy prezentační logiku. Autorizaci řešit může, ale to je jen bonus pro uživatele – autorizace se musí vždy primárně provádět v business vrstvě. Jedná se tzv. cross-cutting concern a může tak být vhodné implementovat ho pomocí aspektově orientovaného programování (AOP). Stránkování a filtrování by měla umět Repository, to je celkem běžné.
Nejsem si jist, co tím posledním odstavcem přesně myslíš, ale možná popisuješ tzv. Application Service, což je vrstva, která používá Domain Services (== business vrstvu).
[41]
Ad ad odtavec 1: Nelibí se ti to správně, jde o porušení SRP a hlavně persistence ignorance doménového objektu.
stránkování a filtrování doporučuju dělat pomocí Query Objectu viz http://rarous.net/…-dotazy.aspx
BTW domain services dost svádí k tomu, že model je anemický.
Komentář smazán autorem blogu pro přílišnou míru zhůvěřilosti.
Nepřeceňoval bych tolik SRP – jak se z čehokoli dělá dogma, jež má být chápáno absolutně, nevěstí to nic dobrého. Active Record sice porušuje SRP, ale zato se více přiklání k principu lokality.
[41]Augi[42]Aleš
V 1. odstavci jsem měl na mysli pouze existenci jakýchsi proxy metod v Entitě, které přímo z Entity volají příslušné metody Repository: např. „public function save() {return $this->repository->save($this);}“. SRP jsem si myslel, že tím neporuším, jen vytvářím prostředníka a usnadňuji tak volání save() na Repository, aniž bych musel s sebou neustále po jednotlivých vrstvách „vláčet“ dvojici Entity i Reposotory současně.
[42]Jojo, anemický model je to, s čím bojuju. Ale četl jsem i články, které tvrdí, že anemický model není apriori špatný.
[43]Že Tě to baví…
[41]Augi
V 6. posledním odstavci jsem se snažil jen naznačit, že prezentační logika řeší v mnoha případech to samé jako business logika: omezuje viditelnost dat, na která nemá uživatel právo a to jak v rámci celých záznamů, jednotlivých sloupců, tak i povolených akcí; řeší uchování persistence obsahu editovaného záznamu, stránkování, filtrování, řazení, vybraný záznam apod.
Proto by nebylo od věci uvažovat o umístění abstrakce té části prezentační logiky, která řeší výše zmíněné, přímo do modelu a těsně svázat s business logikou. Zjednodušeně řečeno, nad vrstvou Service by přibyla ještě volitelná vrstva „Presentation“.
Vrstva Presentation by se tak o svůj obsah starala zcela autonomně (skládala by obsah z různých zdrojů: db, request, session, cookies, cache atd.) a na vyžádání by jej poskytovala ve formě již známých Entit.
Controller by pak dělal prostředníka mezi Presentation a Service, případně by Prezentation metody (odpovídající akcím na stránce) mohly přímo volat odpovídající Service metody business modelu.
K tomu Propelu – sice to navenek vypada jako ActiveRecord, ale vnitrne je to rozvrstvene. Napriklad k tabulce article se vygeneruji objekty Article (entita), ArticleMap (mapper) a ArticlePeer (repository). Takove volani $article->save() vnitrne vola ArticlePeer::doSave(). Krome toho je tam i implementace Query objektu nazvany Criteria.
Propel se generuje z obecnyho datovyho modelu, ale pro konkretni databazovou implementaci, takze si muze dovolit smrsknout nektere vrstvy dohromady, aniz by to melo nejake nevyhody. I kdyz nemusite Propel pouzivat, doporucuju udelat si jednoduchy model a prohlednout si pro inspiraci vygenerovany kod.
[47]Ad omezení dat, pojmenujme to validace a autorizace – umístění těchto záležitostí do Modelu a probublání z Modelu až do Prezentace je IMHO v pohodě. Ale je třeba si uvědomit, že validace a autorizace je součást domény a tedy primárně patří do Modelu. To, že si budeme načítat informace o validacích nebo přístupu i z Prezentace, to je věc druhá (a IMHO na ní není nic špatného). Takže ano, do jisté míry řeší Prezentace něco podobného jako Model, ale validace a autorizace jsou součástí Modelu proto, že je tam potřebujeme, musí se provést vždy, ať se do systému leze přes web, web-service nebo nějak jinak.
To, co popisuješ dále jsou IMHO Application Services, tedy další vrstva nad Doménou, která už může být IMHO specifická pro daného klienta (web, web-service, REST, …).
[47]Můžeš mrknout na [tenhle článek | http://www.lostechies.com/…-design.aspx.
[45] Když dostanu úkol a pouze ho deleguju, jsem za výsledek stejně zodpovědný, jako ten, co ten úkol ve skutečnosti plní.
[21] Už jsem to dokončil a zveřejnil, jmenuje se to NotORM – http://www.notorm.com/
[1] mezi is/get by přece neměl být významový rozdíl – ten je jen v tom, že „isNěco()“ můžu použít u booleovské hodnoty, zatímco v jiných případech musím použít get (resp. pojmenovat si metody můžu jak chci, ale neodpovídalo by to konvencím).
[11] vyčerpávající odpověď, ušetřil jsi mi komentář
[13] „Výsledkem bude, že se vám z hlavy vykouří zažité sr.čky, otevře se hlava a budete čerství a schopni poznat, jakým způsobem se ubírat a jakým ne.“
Hodně lidem bohužel chybí teoretické základy – datové modelování, princip tří architektur – a místo toho si už od samého začátku představují tabulky v MySQL (o těch dvou obecnějších úrovních abstrakce nemají vůbec ponětí).
A co se týče publikuj() jakožto metody třídy Článek – chce to se trochu zamyslet a abstrahovat.* Komu tahle akce (publikovat) přísluší? Kdo je jejím vykonavatelem? Vydá se článek sám od sebe, nebo je to spíš neživá věc, která sama od sebe nic nedělá?
Otázka není, zda to jde nějak zbastlit – otázka je,
jestli to dává smysl, jaká je logika věci.
Pak je ještě otázka, zda dělat anemický doménový model (jen přepravky bez další logiky) nebo v objektech spojovat data a chování (což je přece princip OOP). Trochu o tom psal Dagi: http://www.dagblog.cz/…archive.html Oba přístupy mají svá pro a proti – osobně si myslím, že anemický model je bezpečnější – v tom smyslu, že dá víc práce, aby to člověk totálně „zvoral“ (zvlášť u jednodušších aplikací). Takže s propojováním dat a logiky raději opatrně – člověk prostě musí vědět, co dělá
*) udělat analýzu, kterou mnoho „moderních“ „programátorů“ ignoruje a myslí si, že ji nepotřebují
[53] Anemický model je bezpečnejší – prečo potom programuješ objektovo? To už potom stačia dátové štruktúry a metódy a je to to isté.
Ahoj všem, konečně jsem se dostal k tomu, abych si tu všechno pročetl, promyslel a i dále dohledal. Díky moc všem za komentáře, námitky a připomínky, navazující diskuzi i zajímavé odkazy. Rozhodně mě to v mém rozhledu posunulo zase o velký kus dále. Můj původní záměr hned opravit různá pomýlení či zavádějící terminologii v textu narazil na to, že mi postupem času vyvstaly zase další a další nejasnosti. Zkusím tedy své otázky opět zformulovat v nějakém dalším článku. Ještě jednou všem díky!
[54] A ty děláš jen bezpečné věci? Resp. je bezpečnost jediným/nejdůležitějším kritériem, že takhle reaguješ?
BTW: mnohdy opravdu stačí jen datové struktury, protože hodně webových aplikací je jen „zobrazovač dat z databáze“ a jejich entity žádnou vnitřní logiku nemají. Je celkem hloupé, když si někdo přečte, že anemický model je špatný, a tak najednou začne vytvářet metody a cpát logiku i tam, kam nepatří.
Nenarážal som konkrétne na tú bezpečnosť (to bol len výňatok z tvojho textu), ale na to, že vyzdvihuješ anemický model.
Nie som zástancom toho, pchať logiku tam, kam nepatrí. Ale tvoj príspevok vyznieva tak, že odporúčaš umiestniť logiku do servisnej vrstvy, aj keď je možné dať ju do objektov. To je podľa mňa nesprávna architektúra.
Uznávam, že keď niekto ten objektový model urobiť nevie, môže tým narobiť viac škody ako úžitku. Ale potom je otázne, či nezvolil zlú programovaciu paradigmu. Ale ako radu začiatočníkom to beriem, aj keď by som zdôraznil (tak, ako píšeš), že si treba doplniť vedomosti z objektového modelovania a nezanedbávať analýzu.
„ale na to, že vyzdvihuješ anemický model“
Vždyť já ho ale nevyzdvihuji
Psal jsem že oba přístupy mají pro a proti. Dagiho
článek jsem odkazoval právě proto, že upozorňuje na nevýhody anemického
modelu. Stojí to za přečtení, inspiraci – i kdyby se pak člověk
rozhodl, že stejně zůstane u svých klasických přepravek-struktur bez
vnitřní logiky. A konec konců to, že OOP umožňuje spojovat data a
chování do objektů neznamená, že této vlastnosti musíme u všech
objektů využívat – můžeme mít objekty „živé“, které mají
nějaké svoje chování a pak objekty „neživé“, které mají jen stav,
ale samy od sebe se nijak nechovají, nevyvíjejí žádnou činnost –
odpovídá to reálnému světu.
Objektové programování mělo svou historickou úlohu, ale teď mi přijde jako takový mor, který brání dalšímu vývoji. Navíc Java, C# a podobné jazyky jsou již koncepčně zastaralé a za čistě objektové se považovat rozhodně nedají. Dál už je i třicet let starý Smalltalk.
[59] aha, takže Java a C# jsou zastaralé, Smalltalk je v objektovosti dál, ale zároveň OOP je mor… takže v čem programovat?
Dobrej článek;) nesouhlasim s tim co bylo napsaný výš Tomášem, že se řeší něco dávno prozkoumanýho nebo se objevuje kolo. Znám dobře ORM frameworky jako Hibernate, Doctrine (které vypadá ve verzi 2 dost zajímavě) či Propel. Stejně tak jsem prošel spoustu věcí okolo DDD a mám načtený bible jako jsou knížky of Fowlera, kde popisuje ORM vzory a další related vzory. A stejně řešim jak přistupovat k architektuře aplikace.. potřeba o tom mluvit je.
Dospěl jsem vesměs k tomu samému co tu píšeš, jenom bych varoval před těmi „entitami jako přepravkami dat“.. to, že spojujeme data a funkce, které s nimi souvisí do jednoho celku je přece gró objektovýho přístupu, a jakmile bude entita jenom uloziste pro data tak nám to přestane přinášet jakýkoliv výhody..
Zajímalo by mě co jsi zvolil jako Mapper, nějaké open-source, nebo vlastní řešení? pokud vlastní, co tě k tomu vedlo?
Franta: Velmi provokativní příspěvek, že? :) Já bych totiž rád viděl, aby se programátoři začali více zajímat o funkcionální jazyky a ty se tak dostaly více do praxe. Již jsou totiž na velmi použitelné úrovni. Třeba Haskell.