Proof of concept: Generics
- Filip Procházka
- Moderator | 4668
Někteří z nás používají například Doctrine 2 ORM. Doctrine má
EntityManager
a ten poskytuje pomocí metody
$em->getRepository($entity)
přístup k repozitářům
jednotlivých entit.
Pokud bych chtěl mít repozitáře v DIC, tak není problém, prostě pro každou entitu registruju repozitář
services:
usersRepository:
class: Doctrine\ORM\EntityRepository
factory: @\Doctrine\ORM\EntityManager::getRepository('Acme\User')
Teď si je můžu v pohodě předávat kam budu potřebovat.
class UsersPresenter extends BasePresenter
{
public function __construct(Doctrine\ORM\EntityRepository $users) {}
Ale, co kdybych je tam všechny registroval a chtěl si je nechat přes
autowiring doplňovat? Nette mi to nedovolí, protože vidí jen XY služeb a
všechny mají stejnou třídu. Spousta lidí to „řeší“ tak, že třídu
si podědí, pojmenuje ji UsersRepository
a tu pak vyžaduje (nebo
použije interface). Osobně tohle považuju za anti-pattern a dle mé
zkušenosti to není dobrá cesta.
Někteří možná znají ze staticky typovaných jazyků generika – když to hodně zjednoduším, tak jde o to, že si nadefinuji nějakou základní třídu, která „obaluje“ chování nad určitým typem objektu, jako je třeba náš repozitář. Já mu řeknu, že má pracovat s entitou uživatele a on podle toho skládá dotazy.
Když naučíme Nette DIC rozlišovat, s jakým typem služba pracuje, můžeme si ušetřit práci s konfigurací a nechat si vše předat pomocí autowire. Přesně to (mimo jiné) dělá tento pull request.
Upravím si tedy definici služby
services:
usersRepository:
class: Doctrine\ORM\EntityRepository<Acme\User>
factory: @\Doctrine\ORM\EntityManager::getRepository('Acme\User')
a doplním annotaci do presenteru
class UsersPresenter extends BasePresenter
{
/** @param Doctrine\ORM\EntityRepository<Acme\User> $users */
public function __construct(Doctrine\ORM\EntityRepository $users) {}
V tenhle moment už Nette umí rozlišit, která služba je která a správně je automaticky doplní.
Připravil jsem funkční pull request jako proof of concept a rád bych se dozvěděl, co si o tom myslí komunita a David :)
Samozřejmě je potřeba se smířit s faktem, že bez annotací to nepůjde. Tedy pokud někdo nevymyslí lepší implementaci :)
Editoval HosipLan (4. 7. 2012 18:48)
- Honza Marek
- Člen | 1664
Moc se mi to nezdá, protože Doctrine\ORM\EntityRepository<Acme\User> neznamená v PHP vůbec nic. Ba dokonce to není ani třída, takže to zanáší bordel jak do konfigurace kontejneru, tak do PHPDocu.
- Filip Procházka
- Moderator | 4668
Neřekl bych zrovna bordel, ale je to dost nestandardní, to ano.
Původně jsem to chtěl dělat jako rozšíření, ale narazil jsem na strašně moc překážek. Teď, když mi to funguje, zkusím z toho znovu udělat jako rozšíření. Třeba to tentokrát vymyslím lépe. Taky bych to asi nechtěl přímo v Nette.
//edit: Byl jsem opraven, že generika je trošku širší pojem:) S čímž souhlasím, já s z toho vzal jen to co se hodilo :)
Editoval HosipLan (4. 7. 2012 22:13)
- llook
- Člen | 407
Přijde mi to moc magické a hlavně mám problém s tím, že se to tváří jako něco jiného. Jestli to dobře chápu, tak jde o jakési značkování služeb. Na značkování služeb máme tagy, mě by se víc líbil podobný zápis:
services:
usersRepository:
class: Doctrine\ORM\EntityRepository
factory: @\Doctrine\ORM\EntityManager::getRepository('Acme\User')
tags: [ "Acme\User" ]
Potom zbývá druhá polovina problému: Naučit autowiring předávat
služby podle typu a tagu (nějaké hypotetické
getByTypeAndTag()
). Určitě bych zvolil jinou syntaxi. Takhle to
vypadá jako generický typ a to mate. Také je potřeba zkusit, jak si s tím
nestandardním @param
poradí různá IDE, Apigen a Docblox.
Ale hlavně si nejsem jistý, jestli to je opravdu lepší, než podědění třídy EntityRepository. Proč je to podle tebe anti-pattern?
- Filip Procházka
- Moderator | 4668
K tématu:
@kaja47 napsal(a):
Nesnažím se upřesnit názvosloví, jenom pozoruji.
Ty potřebuješ, aby ti DIC automaticky servíroval různé instance jednoho typu a aby je od sebe rozpoznal, tak používá nějaký tag, v tomhle případě typ objektu se kterým EntityRepository pracuje. Ale tahle asociace není nijak zásadní, je jenom užitečná, protože typ entity v tomhle konkrétním případě rozlišuje jednotlivé instance
EntityRepository
, aleEntityRepository<E>
nemá žádný význam a nepřinese nic jiného kromě práce DIC.Ke generikům by to mělo mnohem blíž, kdyby sis mohl nadefinovat jednu generickou službu,
services: repository<E>: class: Doctrine\ORM\EntityRepository<E> factory: @\Doctrine\ORM\EntityManager::getRepository(get_class(<E>))
A anotace
@param Doctrine\ORM\EntityRepository<Acme\HorseEtity>
by udělala správnou věc ™.S generickými typy pak taky souvisí veselé pojmy jako kovariance a kontravariance a rozlišování, co je podtyp čeho a to není vůbec vůbec jednoduchý :).
Ale teď, když o tom tak přemýšlím, tak vlastně tvoje implementace vlastně dělá přesně to, co jsem napsal nahoře: typu, který má být dodán DICem, ve špičatých závorkách dáváš nějaký tag, který vůbec nemusí být jméno typu/třídy, ale naprosto cokoli.
„Generická služba“ je to, co kdyby z tohohle vzniklo, tak by to bylo úplně nejlepší.
llook napsal(a):
Přijde mi to moc magické a hlavně mám problém s tím, že se to tváří jako něco jiného. Jestli to dobře chápu, tak jde o jakési značkování služeb. Na značkování služeb máme tagy, mě by se víc líbil podobný zápis:
Chápeš to dobře. Zápisů v neonu jde vymyslet mraky. A ano, v současnosti je to v podstatě tag.
Potom zbývá druhá polovina problému: Naučit autowiring předávat služby podle typu a tagu (nějaké hypotetické getByTypeAndTag()). Určitě bych zvolil jinou syntaxi. Takhle to vypadá jako generický typ a to mate.
Podobnost s generickým typem jsem zvolil záměrně, protože z toho v podstatě vycházím.
Také je potřeba zkusit, jak si s tím nestandardním @param poradí různá IDE, Apigen a Docblox.
Schválně jsem ten regulár napsal tak, aby nebyl moc pendant a v pohodě tedy můžu psát cokoliv z následujícího.
@param Foo<Bar>
@param Foo <Bar>
@param Foo <Bar> $foo
@param Foo $foo <Bar>
Osobně testuji třetí zápis a IDE to vubec nevadí, dokonce když dám generovat phpDoc, tak mi to tam nechá, protože to považuje za „poznámku“ :)
Ale hlavně si nejsem jistý, jestli to je opravdu lepší, než podědění třídy EntityRepository. Proč je to podle tebe anti-pattern?
Vycházím z tohoto článku. A ano, jsem si skálopevně jistný, že to co se snažím dosáhnout je ta správná cesta™.
Ještě bych to tedy shrnul. Zápis v Neonu bych moc neřešil. Považuji ho
za správný, protože ho budu chtít rozšířit na to, co popisuje
@kaja47
výše. Chtěl bych se generice přiblížit, ale
v jednodušší formě. Prostě jen přidat službě tento
„chytřejší tag“.
Zrovna mě třeba napadá to znásilnit na chytrou factory
factories:
usersRepository:
class: Doctrine\ORM\EntityRepository
factory: @\Doctrine\ORM\EntityManager::getRepository(%type%)
tags: [ generic ]
ale uvidím, ještě nad tím budu přemýšlet :) Cest jak to implementovat je spousta. Raději bych ale, aby to připomínalo zápis z jiných jazyků, protože se snažím o podobnou věc.
Co mě ale trápí, tak je jak to zapsat v tříde, do které chci injektovat. Já to nemám problém překousnout, ale od ostatních jsem na to ještě nedostal jedinou pozitivní reakci. Asi to bude tím, že pracuju s Doctrine :)
Napadá mě, zavést úplně novou annotaci. Něco jako:
/**
* @genericTypeParameter($bar, Bar<Fuuuu>)
* @genericTypeParameter($baz, Baz<Fuuuu>)
*
* @param Bar $bar
* @param BaZ $baz
*/
public function foo(Bar $bar, Baz $baz) { }
Pokud by někdo měl pěkný nápad, sem s ním :)
Editoval HosipLan (5. 7. 2012 12:06)
- Pavel Kouřil
- Člen | 128
No; mě se to jako nápad rozhodně líbí. :)
Co mi ale vadí na tom @param Foo<Bar> je to, že ty do třídy (a hlavně teda určité standardní anotace) dáváš něco, co způsobuje svým způsobem závislost na určitém kontejneru.
Mnohem víc se mi líbí tedy nějaká anotace ve stylu @Nette\GenericTypeParameter($bar, Foo<Bar>).
- Filip Procházka
- Moderator | 4668
@Pajka: já to raději považuji za „doplňující meta informaci“, která mi usnadňuje používání jednoho konkrétního containeru :) Prostě rozšíření autowiringu. Kdekoliv jinde bych ty služby prostě vyjmenoval, tohle je výrazné ulehčení.
- Filip Procházka
- Moderator | 4668
Update: nový zápis
services:
repository<E>:
class: Doctrine\ORM\EntityRepository
factory: @\Doctrine\ORM\EntityManager::getRepository(<E>)
to <E>
v názvu služby je kratší zápis
pro
services:
repository:
class: Doctrine\ORM\EntityRepository
generic: E # označuje název "parametru s typem"
factory: @\Doctrine\ORM\EntityManager::getRepository(<E>)
Jak je vidět v testech, fungují nové zápisy.
$container->getService('dao', 'User');
$container->getService('dao', 'Article')
$container->getService('dao<User>');
$container->getService('dao<Article>');
$container->getByType('Generic_IDao<User>');
$container->getByType('Generic_IDao<Article>');
Taky jsem si říkal, jestli povolovat oba zápisy, nebo jeden vyhodit, popř
doplnit druhý zápis do getByType()
Teď už to má ke generice trošku blíže. Budu vděčný za další feedback :)
Editoval HosipLan (5. 7. 2012 17:50)
- kaja47
- Člen | 16
Za mě dobrý, jenom bych tomu neříkal generické typy (protože to generické typy nejsou). Možná generické služby.
Kdyby ses vydal cestou typů, pak musíš vyřešit několik zásadních otázek:
Když mám definovanou službu S<X>
a chci S
,
dostanu S<X>
?
Když mám definovanou službu S
a chci S<X>
,
dostanu obcenou službu X
?
Správná odpověď je: „ne“, protože jde o jiné typy (ve smyslu Unboxed newtype).
Ale hlavně to nejdůležitější:
Když mám definovanou službu S<Child>
a chci
S<Parent>
, bude to fungovat (dostanu S<Child>)?
Na první pohled to nevypadá nijak složitě. S<Child> více specifický než S<Parent>, takže když chci něco obecného a dostanu konkrétní implementaci, je všechno v pořádku.
Tak to funguje u tříd, které „produkují“ svůj typový parametr <T>. Když ho konzumují, je to naopak.
Když mám třídu Serializer<T>, která vezme nějaké T a serializuje ho. Mám definovanou službu Serializer<Pony> a někde chci instanci Serializer<Animal>. Když mi DIC dodá Serializer<Pony> (tedy více specifickou třídu) nebude to fungovat, protože já chci serializovat všechny zvířata, ne jenom poníky.
Dává to smysl? Ne? Vítej do světa kovariance a kontravariance, jednoho z nejvíce matoucích pojmů v programování.
Propaguj to jako glorifikovaný tag. Užitečné to bude i bez předstírání generik:
např. mám dvě připojení do databáze: jedno pro archiv a druhé pro živou db, definuji si dvě služby Connection<live> a Connection<archive> a můžu si je nechat injectovat dle libosti.
- Filip Procházka
- Moderator | 4668
U těch rodičů a dětí už jsem ztratil :) Ale dejme tomu, generické služby u mně dobrý :)
Když mám definovanou službu S<X> a chci S, dostanu S<X>?
výjimka, nespecifikoval jsi X pro S
Když mám definovanou službu S a chci S<X>, dostanu obcenou službu X?
výjimka, služba není označena jako generická.
Editoval HosipLan (6. 7. 2012 9:24)
- juzna.cz
- Člen | 248
Ze zkušeností s Javou vím, že generika jsou fakt super věc. Základ je navíc docela jednoduchý na pochopení, ale pak nastanou situace (např. které zmiňuje kaja47) a tam se to začíná celé dost komplikovat. Bohužel bez přesného vyřešní i těch komplikovaných situací se můžeme dostat do slepé uličky.
Mě by se spíše daleko více než DIC líbila podpora v IDE, tzn. aby
fungovalo napovídání u
$em->getRepository('Acme\User')->find($userId)->getXYZ
–
to je zase ale jiná story.
A s tím, co „něco znamená v PHP“, bych si až tak hlavu
nelámal – už teď se v PHP používá hodně věcí, které nic
neznamenají, např. již věrně známé anotace v komentářích nebo zápisy
pole Foo[]
.
Editoval juzna.cz (9. 7. 2012 14:41)
- Filip Procházka
- Moderator | 4668
Napadá tě jak to řešit? Já v tomhle nemám žádné zkušenosti :)
Editoval HosipLan (9. 7. 2012 14:54)
- juzna.cz
- Člen | 248
Pajka napsal(a):
Co mi ale vadí na tom @param Foo<Bar> je to, že ty do třídy (a hlavně teda určité standardní anotace) dáváš něco, co způsobuje svým způsobem závislost na určitém kontejneru.
Tady nezavádíme zádnou závistlost na konkrétní implementaci čehokoliv – jedná se o univerzálně použitelný zápis typu, kterému následně může nějaký DIC rozumět.
Podobně byl zaveden a už se docela ujal zápis polí a'la
Foo[]
místo příliš obecného array
. Původní
zápis, který je navíc validní v PHP, nebyl dostatečně popisný. Už jsme
to zlepšili částečně, tak proč to nezlepšit ještě více? Když někdo
(presenter, služba, …) potřebuje repozitář uživatelů, může si
o něj říci rovnou. To je lepší, než si říct jen o obecný
repozitář a pak kontrolovat, jestli opravdu obsahuje uživatele. Navíc
se tím jasně deklaruje závislost.
Pole něčeho jsou vlastně jednou konkrétní podčástí obecného problému generických typů. PHP samotné generická pole neumí, takže když implementujeme mixér na pomeranče, píšeme:
/**
* @param Orange[] hromádka pomerančů
* @return Juice pomarančový džus
*/
public function mix(array $items) {}
Supr, né? Klasický komentářový hack, na který jsme v PHP zvyklí.
Když budeme aplikovat stejný postup ještě dále, dostaneme o trochu větší třídu generických typů:
/**
* @param EntityRepository <Orange> chytřejší hromádka pomerančů
* @return Juice <Orange> pomarančový džus
*/
public function mix(EntityRepository $items) {}
Opět, co nejde zapsat nativně, musíme dát do anotací. Z pohledu do kódu nebo do API dokumentace je však jasné, jaký že to repozitář máme mixéru dát. Už se tedy nikdo nebude ptát jaký repozitář že to má do mixéru dát (stejně jako dříve se ptal „a to tam mám dát jako pole čeho???“).
Pozn.: V Javě by se psalo Juice<Orange>
(bez mezery),
což by ale rozbilo všechny PHP IDE. Zápis s mezerou je nerozbije.
Další výhoda nastane při popisu mě samého ;)
class Juzna
{
/**
* @param Juice <Orange> juzna nepije jiné druhy džusu!
*/
public function __construct(Juice $drink) { ... }
}
Teď vím nejen jaký repozitář dát do mixéru, ale i že to juzna nenaštve.
Summary: jde tedy o to, abychom mohli detailněji specifikovat závislosti (ve smyslu DI). A když jim pak bude rozumnět i DIC, tak to bude supr.
Editoval juzna.cz (9. 7. 2012 15:32)
- David Grudl
- Nette Core | 8218
To je šikovné. Jen implementace by měla být jiná. Pokud možno bez zásahů do Reflection a NeonAdapteru.
- Filip Procházka
- Moderator | 4668
Reflexi jsem v podstatě jen rozšířil a opravil tam pár věcí, co by se daly považovat za bugy. Jestli chceš, pošlu ti tyhle dva commity jako pullrequest zvlášť.
Co se týče toho ostatního, teď z fleku nevím jak to napsat lépe. Pokud tě něco napadá tak to rád předělám :)
Každopádně, už víc jak týden to spokojeně používám a chrochtám blahem :)
- llook
- Člen | 407
Ten nový zápis vypadá líp, ale nelíbí se mi, že ta služba má s každým parametrem deklarovanou stejnou třídu.
Snad by to šlo řešit tím, že by se ten parametr mohl použít i jako součást názvu třídy:
repository<P>:
class: model\<P>Repository
factory: @orm::getRepository( <P> )
Kontejner by při volání getByType('model\CokoliRepository')
našel shodu s tou maskou, takže by se odvozené repositories mohly injektovat
tím úplně nejméně WTF způsobem:
public function __construct(UsersRepository $users)
{
$this->users = $users;
}
Ale nejsem si jistý, nakolik je vůbec něco podobného potřeba a nakolik jde o zbytečné nabývání syntaxe. Pokud by to bylo v oficiálním Nette, tak bych to používal, ale docela dobře se bez toho obejdu.
- Filip Procházka
- Moderator | 4668
Asi chápu, o co ti jde (napovídání typů výsledků metod). Ale to
co chceš jde proti tomu, o co se snažím a proti čistému kódu. Už
jsem ti to říkal jednou.
Celé tohle dělám, abych ty repozitáře dědit nemusel. Kdybych je chtěl dědit, tak nevymýšlím generické služby.
//edit:
Už jsem si to přečetl pořádně :) Automatické děděni (aby to generoval
container) jsem zvažoval hned na začátku, ale pak jsem to zavrhl, protože
i ty annotace jsou podle mě lepší.
Navíc vem si, taková doctrine… Já si automaticky podědím třídy, ale pak je budu muset zase nastavovat entitám. To je strašně složité. Ale dejme tomu, nějak to ohackuju… Ale co když pak opravdu u jedné chtít udělat výjimku a opravdu ten repozitář podědím, nebo někdo jiný.. jak to pak dostanu do té Doctrine?
Tohle podle mně není řešení.
Padl nápad místo dědění použít class_alias()
. Což sice
zní rozumně, ale napovídání to nepomůže…
Co se týče toho napovídání typů, dědění a upravování annotací je jediná možnost, jak toho se současnými prostředky dosáhnout automatizovaně.
Editoval HosipLan (19. 7. 2012 15:27)
- Honza Marek
- Člen | 1664
Alternativní řešení: Autowiring by mohl v případě nejasnosti podle typu zohledňovat i název proměnné a cpát tam stejně pojmenovanou službu (nebo koneckonců v případě skalární proměnné i parametr).
- blindAlley
- Člen | 31
Možná by ta úprava autowiringu navrhovaná H. Markem mohla řešit i problém https://forum.nette.org/…z-type-hintu. Asi by to chtělo promyslet ty pravidla pro jména parametrů aby se tím pokryly všechny možné zápisy služeb v DIC, i ty, co jsou registrovány přes nějaká DIC rozšíření. Jsem pro tuto alternativu.
- Filip Procházka
- Moderator | 4668
Názvy parametrů možná, ale je to hnusná magie, u které bude víc WTF než užitku. Takhle mě to vždy seřve, že mi chybí parametr a já si ho doplním. To je 1000× lepší, než kdyby se mi tam předal sám a já si nebyl vůbec ničím jistý, že se tam předává ten správný. Přijde po mě nějaký vtipálek, zrefaktoruje mi třídu a najednou to přestane fungovat, nebo hůř, začne se to chovat nepředvídatelně a nikdo to nepozná, dokud to nezničí data na produkci.
A ještě k těm službám: NE. Názvy služeb vůbec třídy nezajímají. Třídy ani neví, že nějaké služby existují! Ani v annotacích! To že to tak dělá Java, není žádný argument.
Editoval HosipLan (3. 8. 2012 11:56)
- David Ďurika
- Člen | 328
HosipLan napsal(a):
Někteří z nás používají například Doctrine 2 ORM. Doctrine má
EntityManager
a ten poskytuje pomocí metody$em->getRepository($entity)
přístup k repozitářům jednotlivých entit.Pokud bych chtěl mít repozitáře v DIC, tak není problém, prostě pro každou entitu registruju repozitář
services: usersRepository: class: Doctrine\ORM\EntityRepository factory: @\Doctrine\ORM\EntityManager::getRepository('Acme\User')
Teď si je můžu v pohodě předávat kam budu potřebovat.
class UsersPresenter extends BasePresenter { public function __construct(Doctrine\ORM\EntityRepository $users) {}
Ale, co kdybych je tam všechny registroval a chtěl si je nechat přes autowiring doplňovat? Nette mi to nedovolí, protože vidí jen XY služeb a všechny mají stejnou třídu. Spousta lidí to „řeší“ tak, že třídu si podědí, pojmenuje ji
UsersRepository
a tu pak vyžaduje (nebo použije interface). Osobně tohle považuju za anti-pattern a dle mé zkušenosti to není dobrá cesta.
tento problem by sa dal elegantne vyriesit cez accessoy a dokonca by to bolo aj lazy
- Filip Procházka
- Moderator | 4668
Zavrhl jsem to, protože je to příliš složité. Řeším to tak, že generické služby emuluju pomocí továrniček, ale musím kvůli tomu pak předání služby konfigurovat.
services:
articles: App\Articles(@doctrine.dao(App\Article))
factories:
doctrine.dao:
create: @\Doctrine\ORM\EntityManager::getRepository(%entityName%)
parameters: [entityName]