DependencyInjection: ServiceLocator vs Context
- wdolek
- Člen | 331
Po dlouhe dobe jsem zavital k Nette, snazim se neco uklohnit (doslova), ale
zas to nechci uplne nabastlit :) … Mohl by mi prosim nekdo vysvetlit, jak je
reseny Context
?
1:
Situace: Mam sluzbu Databazovator
. Sluzby chci mit obecne
dostupne vsude, i kdyz zrovna u teto ocekavam vyskyt jen v casti modelu.
Kazdopadne jsem getContext()
nasel pouze na
Environment
a v instanci presenteru – a to jeste implementovane
kontextovym zpusobem, tedy: Environment::getContext()
je neco
jineho nez (Preseter
) $this->getContext()
(vraci
jinou instanci).
Zkousel jsem si v bootstrapu registrovat nejake vlastni sluzby (potreboval
sem je nejak patricne nastavit, viz nize) a chvilku mi trvalo debugnout
aplikaci, kdyz sem zjistil, ze getContext
v presenteru mi nedava
to co sem nakladl do kontextu pro Environment
(coz je …
kontextove pochopitelne, ze).
2:
Mame v Nette nejaky obecny zpusob, jak nakonfigurovat vlastni sluzby a predavani parametru? Z toho co znam ze „stareho“ Nette, mam napriklad:
service.Nette-Security-IAuthenticator = UsersModel
kde si Nette vyrobi instanci tridy, kterou uvadim. Uz v tomto zapisu nevidim prostor pro nejake nastaveni sluzby.
Bud predanim hodnot konstruktoru po jedne nebo predani v poli… neco jako:
service.Foo = MyFoo
service.Foo.args.name = "kvetak"
service.Foo.args.superValue = 667
od cehoz bych ocekaval:
class MyFoo extends ...Servicator...
{
public function __construct($name, $superValue)
{
...
}
}
$foo = new MyFoo('kvetak', 667);
nebo:
class MyFoo extends ...Servicator...
{
public function __construct($args)
{
...
}
}
$foo = new MyFoo(array(
'name' => 'kvetak',
'superValue' => 667,
));
… mame tu neco takoveho? :)
3:
A jeste jsem drobatko zmaten z te terminologie: ServiceLocator
(stare Nette?), Context
a Configurator
…? Kdyby ve
foru byly smajliky, urcite bych pouzil nejake patricne zmatene :)
… Nebo se pro DI nepouziva vubec getContext
ale uplne neco
jineho?
Editoval wdolek (17. 4. 2011 18:21)
- arron
- Člen | 464
Zaludne otazky:-) Premyslim, jestli pro Tebe nebude uplne nejjednodussi Ti
rict, ze s contextem se da (a podle me je to i nejlepsi) pracovat uplne stejne
jako se starym servicelocatorem s tim, ze se dokonce da pouzit i
Environment::getService()
. Cely ten koncept je takovy silne
nedotazeny a k DI to ma jeste celkem daleko…
- wdolek
- Člen | 331
…ze se dokonce da pouzit i
Environment::getService()
To je dost „nemocknutelne“ pro UnitTestovani – cimz se pak ztraci vyznam pouzivani service locatoru, ne?
Co se bodu 2 tyce, v chatu sem zaznamenal nejake „option“, ktery lze pridat za jmeno sluzby – u slova „option“ ale vidim problem v tom, ze je to „option“ (volitelne nastaveni, volba) a ne „argument“ (povinna hodnota (nebo neco podobneho)).
- Filip Procházka
- Moderator | 4668
1 v Nette\Configurator
(po staru) je
klonování contextu, v momentě kdy se ti vytvoří Application
Environment::getApplication()
tak máš reálně dva contexty.
A co přidáš přeš Environment
nebude v Application, neptej se
mě ale proč. Doporučuju tyhle služby mít už v configu, pak tento problém
nepocítíš.
Takový context by jsi měl předat konkrétnímu modelu,
$model = new KonkretniModel($presenter->getApplication()->getContext());
nebo alespoň službu Databazovatko
, při jeho vytvoření.
$model = new KonkretniModel($presenter->getApplication()->getContext()->getService('Databazovatko'));
2 částečně, když použiješ pole options a pouze pokud máš továrničku…
service.databazovatko.factory = trida::statickaMetoda
service.databazovatko.options.name = "kvetak"
service.databazovatko.options.superValue = 667
class trida
{
public static function statickaMetoda($options) {
return new static($options['name'], $options['superValue']);
}
}
Existuje ovšem hotové řešení, které přidává trošku lepší možnosti konfigurace a právě to co jsi ty popsal, jako očekávané.
Nette\Environment::setConfigurator(new Kdyby\Environment\Configurator);
Ve třídě DefaultServiceFactories máš výchozí konfigurace, je tam ovšem trochu více služeb, než ty asi využiješ, takže když si tohle osekáš a prohlédneš si jak se služby konfigurují, můžeš vytvořením stejně strukturované konfigurace přidávat i služby pomocí configu.
3 Context nemá s DI moc společného, bych řekl. Je to
obecný a vznešeně přejmenovaný ServiceLocator
. To co mu
dodává nádech DI, je až konečné použití ve frameworku. Jeho použití
má nahradit použití Environment v systemu. Až tím vznikne DI :) na DI
přecejenom není potřeba žádná třída, je to styl programování (a
přemýšlení).
- wdolek
- Člen | 331
HosipLan: jup, na to tve DI udelatko jsem na foru narazil, ale spis sem to tak … povrhcne prehlizel, protoze to neni soucasti Nette.
Jinak bych mel prave par vytek k moznym resenim:
- Predavani service locatoru / konkretnu sluzby konstruktorem je mozna efektivni, ale zbytecne pak opakuji kod (pri predani locatoru pak do vsech trid musim pridat locator; pri predavani konkretnich sluzeb se pak mohu vysplhat u nejake tridy ke konstruktoru s N parametry).
- Z tovarnicek jsem vyrostl – nejsem si ted uplne jistej, jak by to slo testovat. Samozrejme v nejakem unit testu bych mohl rict „tak mi vyrob tohle a takhle“, ale bude urcite hodne situaci, kdy nejakou fake tridu jen tak nevyrobim a budu potrebovat mocknout primo service locator – coz statickou tridu nemocknu.*
- Kontextove chovani moc nechapu – prave pro pripad, ktery jsem zminil. Ve
sve aplikaci budu mit nejake kategorie, takze si planuji nejakou sluzbu
CategoryProvider
(nebo tak neco) – sluzba, ktera mi bude podle parametru davat nejake instance, a do ktere budu cpat nove kategorie a ono to zaridi jejich ulozeni do databaze (napriklad) – takoveho providera potrebuju jak v modelovych tridach (jedna kategorie chce najit jinou zavislou kategorii), tak v presenteru (chci vypis kategorii). Pak je sasovina mit dva ruzne kontexty a nekde to haxovat abych v obou mel stejne hodnoty (a pouzitiEnvironment
se mi nelibi – too static).
* respektive nemam kam ulozit nejakou svou testovaci instanci sluzby, tovarnicka je jen tovarnicka…
Editoval wdolek (17. 4. 2011 20:17)
- Filip Procházka
- Moderator | 4668
Nemáš dvě instance služby, ale dvě instance contextu. Protože context, když se klonuje, tak si ponechá referenci na služby. Takže potom máš stejné instance služeb ve dvou contextech, ale přidáváš vždy jenom do jednoho. Proto je potřeba tyto třídy přidávat v configu, aby byly v obou contextech a nedocházelo k zmatení. Prostě přidávat služby až po loadConfig je zlo.
1 proto je tady to „moje udělátko“ (skoro celé to
napsal Patrik, tak abych mu neubíral zásluhy :)
2 to chapu, ja taky, proto mam „lepší context“ :)
//update: však takhle, nebo ne?
Editoval HosipLan (17. 4. 2011 20:22)
- wdolek
- Člen | 331
HosipLan: koukal jsem na ten thread, ktery je v addonech ( https://forum.nette.org/…-zde-opravdu ) + na github, a stale nejsem moc v obraze.
K cemu je ta „sexy“ vec se sluzbami v PHP – snad by melo stacit
„services.ini“ ne? V tom by melo byt uvedeno jmeno sluzby → mapovani na
tridu + dalsi nastaveni dane sluzby. Proc do toho jeste michat
DefaultServiceFactories
? (uz jen to jmeno se mi nelibi)
(V testu bych si pak tu svou sluzbu vytvoril rucne, konfigurak sluzeb by byl pro zivou aplikaci.)
Z ukazky v threadu se mi pak moc nezda predavani sluzeb v konstruktoru. Vzdyt od toho by tu mel byt prave nejaky locator, ktery bych si volal uvnitr te tridy.
A jeste me napada vychytavka – locator by…:
- pomoci anotace
- ze jmena promenne
poznal, kterou sluzbu chci (toto neni z me hlavy)
/**
* @var Foo
* @service Foo
*/
protected function $myService;
/**
* @var Foo
*/
protected function $foo;
… z toho by ale plynulo, ze promenna by nesmela byt private
,
a ze vsechny tridy by musely mit stejneho rodice – proto bych nejakou locator
vec frknul do Nette\Object
.
Navic by to mohlo byt pekne lazy, locator by nevytvarel instanci sluzby
dokavad by skutecne nebyla zapotrebi – to by se posefilo pri
__get()
te promenne (?). (…a tam by se musela udelat takova
oskliva vec – v reflection tu definovanou promennou z tridy zrusit, aby to
pak lezlo pres __get
)
Ale je dost mozne, ze ted mluvim uplne z cesty.
Editoval wdolek (17. 4. 2011 21:49)
- Filip Procházka
- Moderator | 4668
Thread konečně zde, opravdu byl jenom důkaz, že jde vytvořit vrstva mezi presenterem a application, takovým malým „hackem“. A taky, že tyhle továrničky jdou obecně implementovat do nette do contextu.
To na co jsem posílal odkaz je dost pokročilá verze, napsal to Patrik, kompletním refaktoringem contextu a já k tomu přidat taky trošku, ale Patrikovy se moje úpravy nelíbily, takže jsou dvě verze toho stejného, s různým API. Asi bych mohl Patrikovi i říct, že jsem tam našel a opravil pár chyb, ale už si nepamatuji jaké :D
DefaultServiceFactories
je náhrada tohoto: Nette\Configurator,
jde o to, že „moje“ továrničky mají mnohem větší možnosti
konfigurace a proto nahradily většinu původních factories v původním
Configuratoru, ze kterého už se toto nečte.
Když se podíváš na vytváření contextu, tak uvidíš, jak se plní výchozíma službama, které se inicializují defaultně i v samotném Nette. Proto DefaultService-Factories (ty množná čísla mě vždycky trochu zlobila).
Plně z cesty nemluvíš, chápu o co ti jde a ta myšlenka už tu byla.
Když se podíváš na možnosti konfigurace, zjistíš následující strukturu:
"název\\služby" => array(
"factory" => "callback na továrnu, nebo closure",
"class" => "název třídy", // použít pouze třídu, nebo továrnu
"arguments" => array("pole", "argumentů", "pro", "konstruktor"),
"methods" => array(
array("method" => "metoda služby", "arguments" => array("její", "argumenty")),
"aliases" => array("alias", "alias2"), // v serviceContaineru bude přístupné přes ->alias a ->alias2
)
Tuto strukturu určitě v configu napodobit zvládneš, v neonu to jde velice snadno.
Tímto jsi popsal, jak se má služba vytvořit. Služba se nikdy nevytvoří automaticky, je tam jenom skrytej fallback, který reaguje na parametr „run“, který se zadává do konfigurace, v současné implementaci v Nette. (Takže by to mělo umět konfigurovat služby, beze změn v configu)
Další parádní věc je, že argumenty třídy a argumenty method se
prohání přes ServiceContainer. ServiceContainer, se při inicializaci naplní
hodnotami z configu, které jsou pak přístupné pomocí
ArrayAccess
, nebo přes ->getParameter($name)
.
Služby klasicky ->getService($name)
.
Když se pole argumentů prožene přes ServiceContainer, přeloží se hodnoty,
- které začínají zavináčem
@Nejaka\\Sluzba
- vytáhne se název služby a dosadí se výsledek
->getService('Nejaka\\Sluzba')
(lazy instanciace, až když je to potřeba)
- vytáhne se název služby a dosadí se výsledek
- které jsou obalené procenty
%database%
, nebo konkrétně%database[host]%
- vytáhne se klíč a zkusí najít hodnotu, z confiug. Pokud obsahuje
[host]
tak zkusí znovu, v nalezených datech, najít klíč a dosadit hodnotu
- vytáhne se klíč a zkusí najít hodnotu, z confiug. Pokud obsahuje
Můžeš takto nastavovat, jak se mají vytvářet služby, naprosto libovolně, aniž by jsi potřeboval jakékoliv továrničky. A všechno je lazy.
Funguje to z configu (použije se klíč services
) a taky to
nastavuje výchozí služby, které mohou být přenastaveny configem, už při
vytvoření, pro dodržené kompatibility s Nette.
Uff… na něco jsem zapomněl? :)
// update: jo zapomněl:
To co jsi zmiňoval, by znamenalo roztrousit konfiguraci do jednotlivých
tříd a výměna služby, by znamenala zásah do zdrojáku třídy a to
odporuje DI. Proto je konfigurace ServiceContainer
u jen na jednom
místě (na dvou, ale to není podstatné)
Editoval HosipLan (18. 4. 2011 8:06)
- wdolek
- Člen | 331
HosipLan napsal: …
tak az se probudim, pujdu to znovu probadat :D snad se v tom neztratim jako vcera. (muj problem bude asi hodne v tom, ze vlastne nevim, jak to ted funguje v Nette – takze tu mam nejaky kod, ktery Netti chovani vylepsuje, ale kdyz nevim co a jak vylepsuje… tak… ;D ja si to snad najdu)
… a výměna služby, by znamenala zásah do zdrojáku třídy a to odporuje DI
jakto? mel bych v kodu:
/**
* @service db
*/
protected $db = null;
; stare databazovatko
;service.db = MyDBConnectovator
; nove databazovatko
service.db = DbSuperKewlConnector
pro zmenu instance bych sahl do services.ini
a zmenil si to tam.
nebo jsem tu vytku spatne pochopil / a nebo jsem se v predeslem postu spatne
vyjadril (?)
Editoval wdolek (18. 4. 2011 8:33)
- Filip Procházka
- Moderator | 4668
Výměna by samozřejmě mohla proběhnout výměnou implementace služby.
Ale pro ovlivnění konfigurace této konkrétní služby (způsob vytváření) by vyžadoval zásah do kódu a to je proti DI.
- wdolek
- Člen | 331
HosipLan: ja tam ale porad nevidim zasah do kodu, kdyz bych se rozhodl vymenit tridu A za tridu B – v konfiguraci. samozrejme je jasne, ze implementace DI by se musela ze stavajici zmenit (to ale neni proti DI – to je o architekture stavajici aplikace).
no a kdyz nekdo nakonfiguruje sluzbu blbe, tak je to uz jen jeho problem, ne problem DI…
DI uz nema zapotrebi kontrolovat, jakou instanci tomu cpes. proste tomu rikas „pod jmenem sluzby ABC pouzij instanci teto tridy“. kdyz pak nekdo v kodu na te instanci vola neco, co tam neni – spatne si to nakonfiguroval (pouzil neco, co se k tomu nehodi — aka polymorfismus), s tim uz DI nic nezmuze (a ani by to resit nemelo).
a pouzivat s DI nejake tovarnicky – „pokud nekdo chce sluzbu ABC, zavolej tuhle tovarnicku na tomhle objektu/tride a ono to vrati instanci“ to uz mi prijde take pritazene za vlasy. jsem pro ciste reseni – instanciaci tridy, ktera je uvedena v nastaveni sluzeb. jednoduse, ciste, zadny nesmysle kolem.
Editoval wdolek (18. 4. 2011 10:22)
- Filip Procházka
- Moderator | 4668
Nejsem si úplně jistý, jestli tě chápu, ale třída by měla přijímat nějakou službu, která implementuje interface
class trida
{
/** @var iinterface */
private $sluzba;
/**
* @param iinterface $sluzba
*/
public function __construct(iinterface $sluzba)
{
$this->sluzba = $sluzba;
}
}
Pravděpodobně se snažíš vyhnout tomuto. Jde to obejít pomocí reflexe a phpDoc, jak naznačuješ. Ale příjdeš o kontrolu interface přímo ve službě, dá se spoléhat na to, že serviceContainer to nezmrší, ale prostě tam není ta kontrola a je to celé takové vratké.
A stejně si myslím (a věřím tomu, protože to nepoužívá ani symfony) že tím, že řekneš jaké jméno služby chceš mít v jaké vlastnosti takto striktně tak porušuješ inteface injection, nebo tak nějak :P
PS: do „mého“ ServiceContaineru to jde implementovat velice snadno,
stačí si trošku přiohnout třídu ServiceFactory
nebo nic neohýbat a mít metodu setIinterface
a
v konfiguraci mít
'methods' => array(
array('method' => 'setIinterface', 'arguments' => array('@Iinterface'))
)
která vlastnost nastaví (ale má to nevýhodu, že půjde $service změnit i po vytvoření containeru.
Popřemýšlím, nakolik se mi protiví property injection (klidně reflexí) na takovéto úrovni, byl bych pro tu možnost mít, ale definovat co tam patří vždy v configuraci, né ve službě (bez té podpory phpDoc)
Editoval HosipLan (18. 4. 2011 12:07)
- wdolek
- Člen | 331
proc by sluzba mela neco implementovat? jenom proto, aby sis pak byl jisty, ze jsou na objektu takove a makove metody? kdyz sluzbou podstrcis objekt, ktery takove a makove metody nema, aplikace crashne – coz je ale chyba toho, kdo se snazil dat kostku do trojuhelnikove diry.
// nejaky kodik
// na sluzbe 'db' (trida A) volame metodu query
$this->db->query(...);
class A {
public function query()
{
...
}
}
class B {
public function fooBar()
{
...
}
}
pokud zamamlasim, a reknu, ze:
service.db = B
tak je to samozrejme moje mamlaskovina – aplikace spadne na tom, ze
„volana metoda query
nebyla na objektu nalezena“. kdyz
tam pridam jeste kontrolu interface – aplikace mi v dusledku nejake kontroly
stejne spadne (vyhodi se nejaka vyjimka) – v aplikaci pak mam ale
(zbytecnou) kontrolu, ktera prakticky nic neprinasi.
proto bych se spolehal na polymorfismus, a na kontroly rozhrani bych si nehral.
snad se ted ale bavime o tom samem, kdyztak muzeme poklabosit na PS, zacinam v tom svem DI pojeti mit gulas…
- Filip Procházka
- Moderator | 4668
To je právě to, když máš kontrolu interface (nemusí být interface, můžeš tam i nějakou baseclass, to je fuk) tak ti to umře už při vytváření služby. To mi příjde lepší (a evidentně nejenom mně)
Koneckonců, v konstruktoru nemusíš mít vůbec ani vynucené interface (které mají bránit chybnému vytvoření služby), každopádně jde o to, že konfigurace by měla být na jednom místě a téhle myšlenky se nechci vzdávat :)
Jo vidíš, na to jsem zapomněl, autowire je tam implementovany taky :)
Kolik máš tříd, které využívají více jak dvě nebo tři jiné služby? Já teda moc ne :) A když jo, tak je využívají přes jiné
class sluzba implements isluzba
{
public function __construct(ijinasluzba $sluzba)
{
$this->sluzba = $sluzba;
}
public function step()
{
$this->sluzba->move();
}
}
class trida
{
public function __construct(isluzba $sluzba)
{
$this->sluzba = $sluzba;
}
public function doDance()
{
$this->sluzba->step();
$this->sluzba->step();
$this->sluzba->step();
}
}
Takhle nějak :)
- wdolek
- Člen | 331
Kolik máš tříd, které využívají více jak dvě nebo tři jiné služby?
… mno :D tak treba v praci mame megalomanske sluzby vyuzivajici tunu jinych sluzeb. uvazuji v obecne rovine, i kdyz to, na cem nyni pracuji by si skutecne poradilo s predavanim v kontruktoru (ale jako obecny postup se mi to nelibi).
nemluve o tom, ze budes svuj kod rozsirovat, budes pridat novou sluzbu. musis pozmenit konstruktor, pak konstruktory potomku, pak se modlit zes neco nezapomel…
To je právě to, když máš kontrolu interface (nemusí být interface, můžeš tam i nějakou baseclass, to je fuk) tak ti to umře už při vytváření služby. To mi příjde lepší (a evidentně nejenom mně)
pro me to znamena takove male diktatorstvi:
„tak hele panacku, jestli chces pouzivat tuhle tridu a naklast do ni vlastni sluzbu, tak musis implementovat nase rozhrani / rozsirit nasi tridu, jinak si neskrtnes“
proti
„naklad si tam co chces, melo by se to ale chovat jako tohle, a pokud nebude, tak si trouba a nechod k nam pak brecet“
(samozrejme uvedeny pripad je uz trosku extremni, ale jinak nez hyperbolou bych nevyjadril me pocity)
btw. vratme se ke korenum – smalltalk ;D
Editoval wdolek (18. 4. 2011 17:39)
- Filip Procházka
- Moderator | 4668
Nojo, ale ty si asi neuvědomuješ, že to interface tam nemusí být,
protože jednotlivé argumenty si stejně volím v konfiguraci služby a vůbec
tam nemusím mít nějakou typovou kontrolu. Já tam často mám třeba typovku
na konkrétní třídu (Nette\Web\HttpRequest
), abych měl jistotu,
že ty metody tam budou fungovat. Je to sice určitým způsobem buzerace, ale
„tak nějak by to mělo být“, ne? :)
Tvůj pohled já vidím jako „ono to po mě něco chce a to se mi nelíbí“ zároveň s „hele co takhle to udělat úplně laxně a třeba to bude fungovat“. Nechápu proč se bránit určitým hranicím :) Navíc o co se hádáme? O to, že to nemá property injection, nebo že jsem naznačil interfacy v konstruktoru(které nejsou vůbec povinneé)? :)
Pokud chceš, nemuset konfigurovat argumenty služeb, ale plnit je automaticky pak v „mé“ implementaci musíš mít
- konkrétní typ u argumentu konstruktoru
- stejně pojmenovanou službu
- instanci toho typu (nebo jeho potomka/implementace) v serviceContaineru
- nespecifikovat argumenty třídy
Pak se použije autowire a doplní se služby „samy“, ale to jenom taková perlička :)
PS: mělo by to být plně kompatibilní s aktuálním (ještě jsem nepřepsal na nové namespaces) Nette.