DependencyInjection: ServiceLocator vs Context

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
wdolek
Člen | 331
+
0
-

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
+
0
-

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
+
0
-

…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
+
0
-

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
+
0
-

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 pouziti Environment 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
+
0
-

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
+
0
-

no :D tak proc to mygat uz neni soucasti Nette?!

Tharos
Člen | 1030
+
0
-

Toho klonování kontextu jsem si také všiml. Od začátku mi vrtá hlavou, k čemu je dobré? Na DI nejsem odborníkem a rád bych se konkrétně v této věci (klonování) dovzdělal.

Editoval Tharos (17. 4. 2011 20:51)

wdolek
Člen | 331
+
0
-

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
+
0
-

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)
  • 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

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 ServiceContaineru jen na jednom místě (na dvou, ale to není podstatné)

Editoval HosipLan (18. 4. 2011 8:06)

wdolek
Člen | 331
+
0
-

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
+
0
-

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
+
0
-

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)

wdolek
Člen | 331
+
0
-

… a vlastne mi nejak postupem casu dochazi, ze… ;D ten service locator musim do sveho objektu nejak podstrcit, takze me vytky ohledne predavani konstruktorem byly liche bump (sam od sebe se locator v tride neoctne, snad jedine ze bych vse stavel na samotnem locatoru)

Filip Procházka
Moderator | 4668
+
0
-

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
+
0
-

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…

wdolek
Člen | 331
+
0
-

… a ano, nechci predavat jednotlive sluzby konstruktorem. predaval bych akorat service locator, kteremu si pak budu o ruzne sluzby rikat (a pak uz je asi jen na me, jakym zpusobem).

Filip Procházka
Moderator | 4668
+
0
-

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
+
0
-

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)

wdolek
Člen | 331
+
0
-

… a opet po chvilce rozjimani me napadlo, ze nejake neprijemne tlaceni do pouziti konkretni soucasti nejakeho frameworku bych mohl vyresit adapterem %)

Filip Procházka
Moderator | 4668
+
0
-

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.