Přeskočit na hlavní obsah

Kam s obslužnými metodami modelu?

V poslední době často přemýšlím nad správným uchopením obecné architektury aplikace. Zejména části týkající se modelů. To je aktuálně umocněno startem naší nové vývojářské firmy, kde spolu s ostatními kolegy o správném přístupu hodně diskutujeme. Mám spoustu otázek a málo přesvědčivých jednoznačných odpovědí.

Vím, že související diskuze proběhly či probíhají i jinde. Ať už v jiných firmách, či na jiných platformách, než je PHP. Třebas i Ruby on Rails tuším v současnosti prožívají přechod od ActiveRecordu k modelům založeným na mapperech apod.

Zkusím sem proto své otázky postupně trousit po kapkách, abych toho sem nenahrnul příliš najednou. Pokud máte k daným věcem co říct, nestyďte se projevit v komentářích!

Kam s nimi?

Dnešní otázka se týká umístění obslužných metod ke každému modelu. Co je asi jasné, že základní třída bude obsluhovat základní věci, jako jsou gettery a settery. Ideálně tak, že pro položky, které se mají jen číst, ale nikoliv přímo nastavovat (jako je ID nebo datum vytvoření), není vůbec žádný veřejný setter definovaný:

class Article
{
    public function getId() {...};
    public function getCreated() {...};
    public function getTitle() {...};
    public function setTitle($title) {...};
    public function getText() {...};
    public function setText($text) {...};
}

Kde ale mají být obslužné funkce pro práci s databází nebo obecně jakýmkoliv jiným mapperem? Jako je načtení, uložení, smazání nebo posunutí článku nahoru či dolů? Osobně jsem dosud používal přístup, kdy byly definovány přímo v rámci třídy Article:

class Article
{
    public function __construct() {...};
    public static function find($id) {...};
    public function save() {...};
    public function delete() {...};
    public function move($shift) {...};
    ...
}

Teď prosím neřešte, co tyto funkce dělají na pozadí, zda přímo volají nějaké SQL dotazy, nebo zda vše provádí přes nějaký mapper.

Moje otázka se týká toho, že se v tomto ohledu často uvádí, že by tyto obslužné metody neměly být v Article, ale měly by být vyhozeny zvlášť do nějakého repository objektu:

class ArticleRepository
{
    public static function find($id) {...};
    public static function save(Article $instance) {...};
    public static function delete(Article $instance) {...};
    public static function move(Article $instance, $shift) {...};
}

Jaké jsou zásadní důvody pro to, aby se pro to vytvářel takto ještě jeden další objekt? Namísto toho, co by to bylo všechno pěkně pohromadě? V čem je v praxi ten či onen přístup lepší? Můžu s prvním přístupem někdy narazit na nějakou neřešitelnou překážku, kterou by mi druhý přístup skrze repository vyřešil?

Co mne zatím napadlo

  • Single responsibility. Je to i v tomto případě tak zásadní princip? Pokud odhlédnu od akademické čistoty, tak z hlediska praktického – proč?
  • Z repository můžu kdykoliv začít vracet cokoliv jiného, než Article, aniž bych musel přepisovat polovinu aplikace. Například když budu mít různé speciální potomky Article. To ale můžu přece vracet z funkce Article::find() taky. Nebo mi něco uniká?
  • Testovatelnost – zlepší se mi při rozdělení do dvou tříd nějakým způsobem možnosti testovatelnosti? Pokud ano, tak jak?
  • Z ArticleRepository nemůžu přistupovat k privátním proměnným třídy Article, musím je všechny odkrýt jako public, abych je byl schopen naplňovat nebo číst. Například pokud chci mít metodu find() definovanou v ArticleRepo­sitory, musím definovat ve třídě Article veřejné metody setId() a setCreated(). Což se mi vůbec nelíbí.

Možná ještě spousta dalších argumentů a protiargumentů?

Komentáře

  1. Ad posledni otazka)

    Proc nemit data jen v ArticleRepository a neudelat Article jako Proxy/Interface na omezeni pristupu?

    A nebo misto setId a setCreated pouzivat konstruktor?

  2. Povzdech k odkrytí: Java to řeší zajímavým způsobem, kde se v případě vnitřní třídy vygenerují access metody, které dostanou příznaky synthetic a public. Public je jasné. Synthetic znamená, že metoda nepochází z kódu a snaží ji programátorovi maximálně skrýt. Jinou cestou v Javě je viditelnost package-private, tedy celému balíčku. Tolik k povzdechu. Tedy nejde o obecný problém, ale o problém jazyka.

    K oddělení do dvou tříd: Zatím jsem to moc neviděl, ale líbí se mi při oddělení repozitáře možnost vytvářet podrepozitáře. Pro testování to má význam u mocků.

  3. Repository nebo rovnou service → repository se hodí na to kdyz by jsi chtěl použít jako persistentní vrstvu nějakou jinou technologii. Repository bude proste jen implementovat nejake rozhrani a ty napises jen jinou repository. Kdezto kdyz by se ti o ukladani staral primo model tak musis vlastne napsat novej model nebo dedit ale s dedicnosti se vyplati setrit :)

  4. JSP (Spring + Hibernate) to řeší tak, že pomocí frameworku hibernate si můžete pomocí mapovacího XML nastavit rovnou atributy POJO objektu, který zrcadlí položku v databázi. Dají se s tim dělat celkem psí kusy. Ukládání řešeno pomocí DAO. Je to čisté a snadno rozšířitelné. Hlavní výhodou je přehlednost.

    Snad v PHP je nějaký podobný framework, který to řeší podobně.

  5. [1] Proč nepoužívat konstruktor? Protože je občas třeba vytvořit instanci a až po nějakém čase jí nastavit id. Typické situace vypadá tak že:

    I) Vytvořím prázdnou instanci třídy Article.
    II) Nastavím jí titulek a text daty, která jsem převzal z formuláře.
    III) Zažádám o uložení přes databázový mapper poskytující metody save a očekávající parametr typu Article.
    IV) V ukládací metodě mapperu nastavuji id, která jsem získal z INSERT … RETURNING …

  6. [1] Ani tím konstruktorem se mi to nelíbí, stejně je to cesta, jak do instance „vstříknout“ něco, co tam nepatří.

    [3] Persistentní vrstvu můžu dělat až o úroveň dál, nahrazením mapperu. Například místo databázového mapperu tam vložím třeba nějaký Memcache mapper. A nemusím ani dědit třídu, ani zakládat novou repository.

  7. Velmi mi to pripomina Propel vs. Doctrine (v1.2). V Propel je ku kazdej entite (tabulke) vygenerovana dvojica tried – hlavna (Article) a „databazova“ (ArticlePeer). Metody ArticlePeer zvacsa vracaju instancie Article.

    Doctrine 1.2 to ma presne opacne. Vsetky metody (aj obsluzne) su sucastou objektu (Article). Ten ich ziskava (teda tie manipulacne) podedenim od Doctrine_Record. Do hlavnej triedy tak staci dopisat iba hlavny kod (getAuthor(), getCategories() ).

    Dolezite je poznamenat, ze Doctrine sa vo v2.0 od tejto schemy odchyli a modely si uz neponesu DB backend v sebe. Pojde o obycajne PHP objekty. O ich „perzistenciu“ sa bude starat tzv. EntityManager.

    $user = new User();
    $user-setName(‚John‘);
    $em->persist($user);
    $em->flush();

    EM vykonanie SQL dotazov optimalizuje, tak aby boli vykonane co najoptimalnejsie. Preto tak radikalne vo v2.0 zmenili cele ORM. Tvrdia, ze niekdey moze byt tento pristup rychlejsi ako RAW PHP. Viac na http://www.slideshare.net/…-old-php-orm

  8. [6] To znamená ze při každém vytvoření objektu Article je nutné i nastavit mu tuto vrstvu. To bych řekl že vnáší zbytečnou komplexitu kde není potřeba

  9. Tomu repository se tuším obvykle říká DAO (data access object). Pokud (jak píšete) nemáme řešit, jakým způsobem se přistupuje k databázi (tzn. může a nemusí tam být další databázová vrstva), pak je DAO jednoznačně potřeba už proto aby se nemíchala business logika s přístupem k databázi.

    Pokud se předpokládá nějaká databázová vrstva s hodně stabilním rozhraním (nějaký standard, jestli PHP něco takového má), tak tam možná DAO nemá význam, ale vývojáři se na tom moc neshodnou.

    Příklad – Java EE disponuje standardizovaným objektovým rozhraním pro přístup k databázi (JPA), které je dostatečně abstraktní. Pěkné shrnutí diskusí o tom, zda je v takovém případě DAO potřeba je k nalezení zde:
    http://www.infoq.com/…7/09/jpa-dao

    Najdete tam i argumenty, proč DAO ano, které se na první pohled dost překrývají s tím, co píšete.

  10. ARTICLE vnímám jako POJO objekt, a měl by tak i zůstat, lze nad ním provádět např. DTO transformaci pro jiné vrstvy, což z praxe považuji za velmi užitečné.

    No a ArticleRepository – chápu jej jako DAO a přijde, že je to reuse přístup mít jej oddělené od doménového objektu – tj. jej lze injectovat do jiných implementací, např. využít jeho rozhraní pro servis vrstvu pro jiné doménové objekty.

    Testovatelnost: určitě, není nic snažšího, než volat pouze metody dao/mapper a to zvlášť. Testovatelnost sql dotazu tímto způsobem jednoznačně nejlepší a přímočarý způsob. Testy jsou velmi triviální a přehledné.

  11. ad)
    Z ArticleRepository nemůžu přistupovat k privátním proměnným třídy Article, musím je všechny odkrýt jako public.

    Neni pravda :). Da se to obejit skrz reflexi, plus sou na to i jine triky, doporucuji precist clanek:
    http://www.doctrine-project.org/…tructor-back

    Sou tam odkazy plus nake povidani kolem, je to sice tak trochu skaredost, ale myslim ze na tomto oknkretnim miste to ma smysl.

  12. Nazdárek, nevím, jestli to bylo v komentářích už zmíněno, ale mě se obecně hrozně líbí styl popsaný zde http://rarous.net/…-dotazy.aspx. Je to sice popsáno v prostředí .NET, ale myslím, že uspokojivě odpovídá na tvou otázku.

  13. K tvým bodům na konci článku:

    1. SRP není „zásadní“ princip v „určitých případech“, ale spíše takové obecné vodítko dobrým objektovým návrhem, na kterém se dokázalo shodnout překvapivě mnoho vývojářů od různých technologií. Moje zkušenost je taková, že držet se SRP vede k lepšímu a flexibilnějšímu designu, ale tento názor si asi člověk musí vybudovat sám vlastním kódem. Jen poznámka: SRP není nic akademického, je to velmi praktická věc.
    2. Myslím, že ti nic neuniká, Article::save() vs. ArticleRepository je převážně o čistotě návrhu, ne o tom, co můžeš či nemůžeš technicky udělat.
    3. Testovatelnost se výrazně nezlepší, ale testy budou logičtější – testy pro Article budou kontrolovat doménová pravidla („text nesmí být kratší než 1 znak“ a podobně), testy pro Repository zase persistenci atd.
    4. To je velmi dobrý postřeh a tady se dá říct jen jedno – smiř se s tím, že testovatelnost kódu má dopad na jeho API. Pokud se na testovací kód díváš jako na další typ klientského kódu, dává to i celkem smysl (musím ale přiznat, že s tím taky stále mám trochu problém).

    Jinak hodně odpovědí na tvé otázky ti dají knížky o DDD (Domain Driven Design), dobrá je např. Applying Domain-Driven Design and Patterns od Jimmy Nilssona.

    Hodně štěstí!

  14. (formátování trošku zahaprovalo, měl to být číslovaný seznam)

  15. Asi bych volil ArticleRepository, ovšem nikoliv se statickými
    metodami. Lze pak nadefinovat příslušná rozhraní a využít faktu, že
    repository je pak objekt jako každý jiný, tj. předávám ho jako
    parametr, vrací se mi z metod, můžu testovat pomocí typeof atd. V tom pak vidím výhodu ve snadné dynamické rekonfigurova­telnosti aplikace
    a menší vzájemné provázanosti jejích části. Zavolám si v rámci nějakého modulu metodu, ta mi vrátí repository a jestli je to teď DbRepository a podruhé to bude XmlRepository, je mi celkem fuk, pokud je poznám podle rozhraní, které zrovna očekávám.

    Těch různých „balíčků funkcionalit“, které nakonec budu chtít nad objekty provádět, může být nakonec celkem hodně a některé možná budu používat podobně často jako ty CRUD metody (indexovat do fulltextu, exportovat/im­portovat, verzovat,…) a mám je všechny vystavovat přímo v Article (či jeho předcích)?

    Další potenciálně zajímavá možnost je vytvořit více instancí „repository“ současně z různých zdrojů a objekty mezi nimi kopírovat, přesouvat, porovnávat, slučovat výpisy atd.

  16. primlouval bych se k DAO jak zminil radek v devatem prispevku :)

  17. Taky jsem dlouho řešil. Jestli používat ActiveRecord, DataMapper, jak a odkud co volat..

    SRP není akademická věc, myslim že vede k přehlednému kódu, architektura aplikace je pak daleko čitelnější. Jakmile se začne aplikace rozšiřovat a persistence a doménový objekty se míchaj dohormady, domain objekty začnou hrozně bobtnat a je těžké se v nich orientovat. Když srovnám starší projekty kde používáme buď ActiveRecord nebo nějaký gateway pattern, se současnými projekty kde je DataMapper, předhlednost a údržba je v druhém případě daleko jednodušší..

    Co se týče toho, že se pak musí zpřístupnit všechny vlastnosti, které se mají načítat, je to tak.. to je trochu daň za použití mapperu, ale neřek bych že nijak velká…

    Procházel jsem několik frameworků a zjišťoval jak to řeší.. Líbí se mi přístup Doctrine 2, bohužel v alpha verzi. Nejlíp model asi řeší framework Flow3 (flow3.typo3.org), tam se jde mimochodem inspirovat se spoustě dalších věcí. A přečíst něco o DDD, jak už psal někdo výše, to obsahuje spoustu odpovědí na to jak vůbec přistupovat k modelu

  18. Dost značně odbočím a zeptám se na něco jiného, Proč pojmenováváš parametry takhle:
    … move(Article $instance, $shift) {…}; ?

    Neříkám že je to špatně, je to dost o konvencích, ale nevidím jediný důvod proč to dělat právě takhle. Jasně, každý se může podívat že $instance je Article, ale uvnitř metody to není moc self-exlanatory.

    Děláš to tak i v projektech nebo se tady jedná jen o příklad?

    Jinak co se probíraného problému týká, domnívám se (a teď schválně reaguji pouze na článek), měly být obslužné metody vytaženy ven. A sice z toho prostého důvodu, že můžeš zjistit, že se články můžeš chtít provádět i operace které prostě nepatří instancím a mohl bys zkončit s hromadou statických metod a zjistit, že třída Article je nějaký hybrid mezi Library a Containerem na dva stringy. Dalším důvodem je to, že tak můžeš značně omezit overahead celé aplikace. Articl repository nemusí článek jen ukládat ale zároveň ho umístit i do vnitřního bufferu a ulevíš databázi. V případě, že budeš chtít provádět na článcích nějaké hromadné operace, je třída ArticleRepository schopná to udělat aniž bys musel vytvářet instanci Article pro každý článek v databázi a přitom udrží rozhranní. Jako příklad vezmu trochu kostrbatou záležitost ale nic lepšího mě v tuhle chvíli nenapadlo. Vygenerování tag cloudu jsi takhle schopnej udělat přímo z ArticleRepo­sitory. Když bys použil možnost číslo jedna, nezbyde ti nic jiného, než napsat třídu TagCloud, což bude třída s jednou jedinou metodou která nedělá nic jiného, než že vytáhne něco z databáze. A budeš se na muset ohlížet až se rozhodněš změnit návrh databáze.

    Další možností, kterou jsem teď vídal poměrně často ale která se mi vůbec nezdá, je použít možnost číslo 2 ale ty dvě třídy zase sloučit tak, že nacpeš ArticleRepository ve formě statických metod zpátky do Article. Ale tak to může v řípadě první možnosti nakonec dopadnout taky.

    Takže za mě, pokud je výběr na těhle dvou možnsot, jsem rozhodně pro ArticleRepository.

  19. [18] Název $instance je opravdu jenom pro obecné účely příkladu v článku, v praxi je samozřejmě lepší to nazývat $article :)