Chytré Entity s inject – skze injected factory?

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Tomáš Votruba
Moderator | 1114
+
-1
-

Zvažuji 5vrstý model, ale nerozumím jedné věci.

Mám entitu User po které chci, aby mi řekla, jaké produkty uživatel přidal. K tomu potřebuji
$productService, ale nevím, jak ji do entity dostat.
Napadlo mne injected factories. U toho zase nevím, jak efektivně převádět každý záznám z databáze na entitu. Té ještě musím předat přes kontruktor samotný záznam.

Kdysi jsem pracoval na projektu, kde se tato věc řešila přes statické metody, což nechcu.
Používám NotORM, což imho není relevantní. Rád bych obecné řešení.

Takhle by se mi líbilo, kdyby to fungovalo :)

class ProfilePresenter extends BasePresenter
{
	$user = $this->userService->fetch($this->user->id);
	$this->template->userProducts = $user->fetchProducts();

}

class User extends Entity
{
	/** @inject ProductService */
	public $productService;

	/**
	 * @return NotORM_Result
	 */
	public function fetchProducts()
	{
		return $this->productService->where('user_id', $this->id);
	}

}

Jaký by byl podle vás nejjednoduší způsob toho dosáhnout?

leninzprahy
Člen | 150
+
0
-

Musí to být určitě metoda té entity?

Neměla by entita jen přenášet data a obstarávat jednoduché operace s nimi (konverze, validace)?

Proč nepoužít

class ProfilePresenter extends BasePresenter
{

    function renderXy() {
        $this->template->userProducts = $this->productService->fetchByUser($this->user->id);
    }


}
Šaman
Člen | 2635
+
0
-

U ORM (např Doctrine, nebo LeanMapper) kde jsou pravé entity by se to řešilo jednoduše:
$user->products; s tím, že ti to vrátí zase entity a nikoliv NotORM_Result

U toho příkladu, který popisuješ bych si buď vytvořil fasádu (servisu) třeba UserProductService, která bude mít metodu getUserProducts($id), případně mít podobnou metodu v ProductServise a řešit to jak psal @leninzprahy.

Ledaže by NotORM uměl najít záznamy ve vazbě 1..N a dostal by ses na všechny produkty jen se znalostí entity (Row) User.

Felix
Nette Core | 1189
+
0
-

Entita sama o sobe by mela byt celkem hloupa a resit maximalne validaci dat. Pouzij IMHO bud DAO nebo Service.

Šaman
Člen | 2635
+
0
-

Tharosova přednáška na poslední sobotě mě přesvědčila, že entita může být i poměrně chytrá, pokud se to udělá rozumně. Injectovat do ní další repozitáře však rozumné není – to už začínáš vytvářet vlastní ORM a tam to dělá vyšší vrstva abstrakce.
Ale NotORM asi bude mít nástroj, jak se na ty vazby dostat z původní entity (tedy bez cílového repa). Něco jako $user->relate…

mystik
Člen | 291
+
0
-

Nedávno jsme řešili něco podobného a skončili jsme s takovýmhle řešením.

Máme service EntityFactory:

<?php
class EntityFactory {

  /** @var \Nette\DI\Container */
  protected $container;

  /**
   * @param Container $container Container from which injects is called
   */
  public function __construct(Container $container) {
    $this->container = $container;
  }

  /**
   * Create new Entity instance and injects all Dao objects
   * @param string $className Entity class
   * @return BaseEntity
   */
  public function create($className) {
    $entity = new $className();
    $this->container->callInjects($entity);
    return $entity;
  }

}
?>

Máme vlastní vrstvu DAO (repository), která nevrací výsledky jako instance pod ní umístěného ORM, ale jako naše Entity, nebo EntityCollection. Prostě konkrétní implementace repository použije nějaké ORM, cache, vzdálené volání, apod. a výsledek převedena na Entity z doménového modelu. To dělá tak, že má injectnutou EntityFactory a převádí tak všechno na naše Entity, které dostanou injectnuté další závislosti.

Felix: Entity by rozhodně neměla být jen obálka na data. Viz anti-pattern https://en.wikipedia.org/…domain_model

Editoval mystik (30. 10. 2013 8:24)

Casper
Člen | 253
+
0
-

@mystik:

Tohle řešení jsem chtěl také realizovat (nad LeanMapperem, s použitím generovaných továrniček), ačkoliv zde určitě najdeš odpůrce, kteří ti řeknou, že entita si nemá co vyžadovat nějaké závislosti :) Nicméně může nastat problém, pokud k vytváření entit nedochází jen v Repository, což je právě případ LeanMapperu. Aneb musí tvé ORM podporovat vytváření entit pomocí nějakých továrniček.

mystik
Člen | 291
+
0
-

Casper napsal(a):

@mystik:

Tohle řešení jsem chtěl také realizovat (nad LeanMapperem, s použitím generovaných továrniček), ačkoliv zde určitě najdeš odpůrce, kteří ti řeknou, že entita si nemá co vyžadovat nějaké závislosti :) Nicméně může nastat problém, pokud k vytváření entit nedochází jen v Repository, což je právě případ LeanMapperu. Aneb musí tvé ORM podporovat vytváření entit pomocí nějakých továrniček.

No jaké jiné vytváření máš na mysli? Napadají mě 2 možnosti:

  1. Lazy kolekce – ty máme řešené vazbou na Repository. Tj. repository vrátí kolekci, které je schopná si z repository data donačítat lazy. Samozřejmě že tohle je před uživatelem té kolekce skryté. Kolekce je jen interface a každá repository si sama řeší jak vypadá její implementace pro ni.
  2. Vytváření nové Entity – to máme řešené tak, že prostě entitu vytvoříš klasicky konstruktorem. Takhle volně vytvořená entity ale nemá žádné vazby – podle mě by ani mít neměla dokud není persistované přes repository. Takže snaha o použití jejich metod, které používají injectnuté věci vyhodí exception, že je ji nutné nejdříve persistovat.
Jan Suchánek
Člen | 404
+
0
-

@Casper: Asi se pletu, ale co Kdyby za vytvořením Entity i prázdné, stál vždy a pouze jen jeho Repository. $this->repository->createEnity(‚User‘);

Casper
Člen | 253
+
0
-

@mystik

LeanMapper má trochu netradiční filozofii ORM k dosažení určitých cílů, takže související vazební entity vytváří i v entitách (vnitřně). Díky tomu umožňuje pokládat konstatní počet dotazů jako NotORM a snadno traverzovat přes vazební entity.

K dosažení toho co říkáš ty bych musel do LM sahat tak, aby se kontejner předával mezi repository a entitami při jejich vytváření (možná si to někdy forknu a zkusím).

Casper
Člen | 253
+
0
-

@jenicek:

To bys právě nemohl traverzovat přes vazební entity. Podívej se do LeanMapper/Entity jak dochází k vytváření vazebních entit např. pro m:hasOne.

Edit:
A vytváření prázdné entity není takový problém, stejně plnohodnotnou práci s entitou můžeš dělat až po její perzistenci.

Editoval Casper (30. 10. 2013 9:14)

mystik
Člen | 291
+
0
-

Casper napsal(a):

@mystik

LeanMapper má trochu netradiční filozofii ORM k dosažení určitých cílů, takže související vazební entity vytváří i v entitách (vnitřně). Díky tomu umožňuje pokládat konstatní počet dotazů jako NotORM a snadno traverzovat přes vazební entity.

K dosažení toho co říkáš ty bych musel do LM sahat tak, aby se kontejner předával mezi repository a entitami při jejich vytváření (možná si to někdy forknu a zkusím).

No my v entitách to řešíme tak, že tyhle related věci stejně vždycky načítáme přes repository. Má to svoje důvody, protože u projektů, kterej řešíme se dá očekávat, že tam bude více než jeden způsob perzistence. V zásadě ale jde o to, že my v entitě máme něco jako:

<?php
  public function getMainAdministrator() {
    return $this->getUserDao()->fetchMainAdministratorOfEvent($this);
  }
?>

O to položení related dotazu a podobně se pak už stará UserDao a ven dostaneš kolekci entit s injectnutýma závislostma. Interně to UserDao ale může fungovat stejně jako v LeanMapperu.

Casper
Člen | 253
+
0
-

@mystik:

Jasné, já jsem měl kdysi požadavek na autora LM, zda by to nešlo všechno vytvářet přes Repository, nicméně jsem neuspěl :)

Nicméně pokud to chápu dobře, na všechny entitní vztahy si musíš napsat takovouhle metodu cos uvedl ne? To právě s LM vůbec nemusím, vše je plně automatické.

Editoval Casper (30. 10. 2013 9:39)

Tharos
Člen | 1030
+
0
-

@Casper: Ten požadavek měla řada lidí :).

@mystik: Smím se zeptat, jak řešíte problém N+1? Klidně jenom nástřel řešení… Zkrátka jak postupujete v případě, že někdo iteruje nad více entitami a pro každou zavolá getMainAdministrator. Díky!

Editoval Tharos (30. 10. 2013 9:49)

mystik
Člen | 291
+
0
-

Tharos napsal(a):

@Casper: Ten požadavek měla řada lidí :).

@mystik: Smím se zeptat, jak řešíte problém N+1? Klidně jenom nástřel řešení… Zkrátka jak postupujete v případě, že někdo iteruje nad více entitami a pro každou zavolá getMainAdministrator. Díky!

No aktuálně nijak :-) Myšlenka je ale asi taková, že by se využily mechanismy, která nabízí ORm, které to naše repository obaluje pokud by to šlo.

Jen nástřel, chtělo by to asi domyslet:

<?php

class Event extends Entity {
 //...
}

class LeanMapperEvent extends Event {

  public function __construct(Row $row) {
    $this->row = $row;
  }

  public function getLeanMapperEntity() {
    return $row;
  }
}

public function fetchMainAdministratorOfEvent(Event $event) {
  if ($event instanceof LeanMapperEntity) {
    $administrator = $event->getLeanMapperEntity()->admin;
    return $this->rowToEntity($administrator);
  } else {
    // mame predanou entitu, ktera nebyla nactena pres LeanMapper
    // musime udelat fetch po jednom
  }
}
?>

Základní myšlenka je nechat práci na ORM, pokud to jde, ale z pohledu aplikace používat jen Entity a kolekce doménového modelu. Tj. doménové entity mít jako rozhraní, které může zabalovat chytřejší entity nějaké pod ním uvedeného ORM, ale bez nutnosti se na něj vázat.

Ale jak říkám tohle je zatím jen úvaha a chtělo by to domyslet případná pro a proti.

U nás je trochu nestandardní situace, protože plánujeme architekturu, kde je dost možné, že třeba uživatelé a události nebudou ukládané v rámci jedné databáze, ale například uživatelé budou načítaní přes nějakou vzdálenou službu. Proto musíme důkladně řešit odstínění od jakéhokoliv ORM.

Tharos
Člen | 1030
+
0
-

@mystik:

Díky za odpověď. Pár věcí rozvedu:

Felix: Entity by rozhodně neměla být jen obálka na data. Viz anti-pattern https://en.wikipedia.org/…domain_model

Můj názor na tohle je takový, že to nelze paušálně říct. Povaha aplikace může být extrémně různá, viz ten knihovní IS vs. hra v mém příspěvku na Poslední sobotě.

Programujeme-li aplikaci s velmi strohou (až žádnou) doménovou logikou, třeba jenom nějaký CRUD, víceméně nepotřebujeme žádný model. Klidně můžeme klást dotazy přímo z presenterů a nikdy s tím nenarazíme na problém.

Programujeme-li aplikaci s velmi bohatou doménovou logikou (což ale IMHO webové aplikace většinou nebývají), pak je škoda skončit úplně s anémickým modelem, protože v tom kontextu může jít skutečně o anti-pattern. Prostě existuje lepší řešení.

Většina webových aplikací, jaké alespoň já programuji, je někde mezi. Nějakou doménovou logiku obsahují, ale není to nic závratného. Část doménové logiky v entitách ponechávám (validace, dopočty nějakých hodnot), část ji mám ve vrstvě nad těmi entitami. Takže v podstatě anemický model. No a nemám s tím žádný problém :–P. Má to své výhody a nevýhody, které jsem schopen vyjmenovat, a ty výhody mi zde převažují.


Všimni si, že to, zda je či není anemický model anti-pattern, se všemožně na netu horlivě diskutuje. Z toho je patrné, že to není tak černobílé. :) Mně se třeba moc líbí tenhle pohled na věc.

Myslím, že Ty tíhneš spíš k plnokrevnému doménovému modelu, zatímco Lean Mapper je lépe připravený na anémický model. Pokud máš opravdu tak košatou aplikaci, že výhody plnokrevného doménového modelu oceníš (anebo jej opravdu chceš), říkám otevřeně, že Ti bude možná lépe vyhovovat Doctrine 2 anebo úplně ruční O/R mapování – to bych v takovém případě možná volil já. Lean Mapper je prostě lean a taky ne úplně vše spásný.

Editoval Tharos (30. 10. 2013 10:52)

Tharos
Člen | 1030
+
0
-

@Schmutzka:

Zvažuji 5vrstý model, ale nerozumím jedné věci.

Ještě jenom takový postřeh k původnímu dotazu… „Pětivrstvý model“ tak, jak jej kdysi popsal Honza Tichý, je anemický model jak vyšitý. :) Taky tam na to v komentářích hodně lidí upozorňuje. Má úplně charakteristické rysy anemického modelu – „tupé“ entity, nad kterými operuje nějaká servisní vrstva atp.

IMHO je Tvým řešením:

1) Smířit s anemickým modelem (mezi námi… existují mnohem horší praktiky, které se denně používají a nikdo se nad tím moc nepozastavuje). Pak je ale zbytečné cokoliv dalšího do entity injektovat a potřebnou logiku je výhodnější mít ve vrstvách nad ní.

2) Naimplementovat si plnokrevný DDD model. Hezky si navrhnout class diagram, vztahy mezi třídami, jak se budou realizovat jednotlivé use-case. V takovém modelu budou mít entity stoprocentně i řadu závislostí na různé třídy, které do nich musíš nějak propašovat. To, co by někdo řekl, že je špatně, tak v DDD je naopak správně. :)

mystik
Člen | 291
+
0
-

@Tharos:

Více méně souhlas. Pro jednoduché CRUD aplikace hloupé entity plně stačí. Vždycky je nutné uvažovat o nejvhodnějším nástroji pro daný účel.

Na druhou stranu dle mojí zkušenosti jsou častější webové aplikace, které nějakou business logiku obsahují. Ale to je asi tím na jakých projektech jsme pracovali.

Tohle rozhodně neměly být nějaká kritika LeanMapperu – popravdě to naše řešení se jím dost inspirovalo :-)

Rozhodně ale nemůžu souhlasit s tím, že Entity BY MĚLY být jen hloupé obálky na data. Podle mě by měly být chytré, ale s tím, že se z toho dá občas ustoupit. Hloupé entity totiž porušují některé principy, podle kterých pracuju s objektovým návrhem. Třeba information expert. Více viz třeba můj článek na zdrojáku http://www.zdrojak.cz/…t-a-creator/

Taky je mi bližší DDD – tj. začínat doménovými entitami a pak vyřešit jejich perzistenci a ne naopak.

To ale nevylučuje použití ORM typu LeanMapper. Jde ale o to, že entity a kolekce, se kterými pracuje doménová logika jsou naše vlastní typy obalující typy použitého ORM. Tj. Repository není ORM, ale Repository používá ORM.

Tharos
Člen | 1030
+
0
-

@mystik: Koukám, že se asi slušně shodneme. :) Já rozhodně netvrdím, že by entita měla být jen hloupou obálkou.

Typickou mou webovou aplikaci tvoří CRUD (coby základ) obohacený o nějakou drobnou doménovou logiku (rozuměj nad rámek vztahů mezi entitami atp.). Tu logiku já typicky z části schovám do entit a z části do „servisní“ vrstvy nad entitami.

  • logiku, která operuje jen nad daty entity, snad ve sto procentech případů umístím do entity
  • logiku, která vyžaduje nějaké další služby, skoro vždycky dávám do té „servisní“ vrstvy

Lean Mapper je na takovýto přístup víceméně optimalizovaný.

Tím, že část logiky dávám do servisní vrstvy, tíhnu k anémickému modelu. Ale až tak mě to netrápí, protože tak činím vědomě, jsem si to schopen obhájit ;), a rozhodně to má i své výhody (snadné injektování služeb do té servisní třídy atp.). V čistokrevném DDD by pak maximum měla pojmout entita a ty servisní třídy by v podstatě vůbec neměly být zapotřebí.

Jinak přidávám odkaz na ještě jeden zajímavý článek :), který mě kdysi dost uklidnil, že se nemusím o DDD snažit „za každou cenu“.


Výhody, které shledávám na „víceméně“ anémickém modelu používajícího servisy, fasády atp.:

  • Je to velmi jednoduché na pochopení, průhledné a zároveň robustní. Byť se plně nevyužívá potenciálu OOP (data a metody s nimi operujícími jsou udržovány při sobě), vystačí na opravdu hodně věcí, aniž by se někde opakovala logika či přetékala vyloženě pryč z modelu.
  • Je to implementačně jednoduché, velmi snadno se v tom injektují závislosti atp.
  • Díky článku od Honzy Tichého to má u nás velkou tradici ;).

Co mi nejvíce vadí na čistém DDD modelu:

  • Je velmi náročný na správný návrh a implementaci. Programátor musí být opravdu zdatný architekt a navíc se spoustou zkušeností, protože jinak je výsledek horší, než anémický model (často výrazně horší). Asi bych se do složitějšího doménového modelu nepouštěl bez modelování.
  • Vyšší nároky na nástroje. ORM musí umět injektovat do entit potřebné služby atp.
  • Je to upovídanější a to u jednoduchých zadání IMHO vytváří zbytečný overhead. Myslím, že anémický model se snáze „zmagičťuje“, vleze se na mnohem méně řádků.

Zásadní výhoda DDD modelu pak samozřejmě spočívá v tom, že pokud je doménová logika opravdu rozsáhlá, naplno využívá potenciálu OOP a vede víceméně k optimální implementaci (z hlediska čitelnosti, robustnosti, testovatelnosti atp).

Editoval Tharos (30. 10. 2013 13:33)

Tomáš Votruba
Moderator | 1114
+
0
-

Vy jste se teda rozjeli OT.

Můj dotaz: chytré entity snadno a rychle.

@mystik: To vypadá pro mě použitelně, díky. Repository vytváří ze záznamů entity na úrovni metod typu fetch() a fetchAll()? To už bych si dokázal představit.

@Tharos: Můžeš rozvést tu 2? Téma * modelů je mi cizí, používám BaseModel a to je vše.

mystik
Člen | 291
+
0
-

Schmutzka napsal(a):
@mystik: To vypadá pro mě použitelně, díky. Repository vytváří ze záznamů entity na úrovni metod typu fetch() a fetchAll()? To už bych si dokázal představit.

Přesně tak. Repository je obálka nad ORM, která vrací resultset z ORM přeložený (zabalený) do Entity nebo EntityCollection.

Tharos
Člen | 1030
+
0
-

@Schmutzka: Ad 2) Jde o tento přístup.

Tomáš Votruba
Moderator | 1114
+
0
-

@Tharos: Měl bys nějaký praktický příklad s kódem? Učení teorií je pro mě pomalé.

Tharos
Člen | 1030
+
0
-

@Schmutzka: Hezkou a stručnou ukázku najdeš například zde.

Tomáš Votruba
Moderator | 1114
+
0
-

@Tharos: To je parádní, díky. Na první pohled to vypadá, že entita je v tomto případě schopna dělat cokoliv, kam by se jinak musela předávat jako vstupní parametr (odeslat email uživateli, registrace uživatele…).

Informací k inkubaci už mám dost, díky všem.

Tharos
Člen | 1030
+
0
-

@Schmutzka: Je to v podstatě tak. Ona ty věci ta entita sama nedělá, také je deleguje, ale dost věcí se realizuje skrze API entity.

Takto navržený model má své výhody (např. robustnost, plné využití potenciálu OOP…) i nevýhody (např. vyšší nároky na kvalitní návrh, vyšší nároky na ORM…).