Potlačení kruhovych závislostí při vytváří služeb v DI

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

Ahoj, napsal jsem si vlastní jednoduchou extenzi pro config. Je to extenze pro model a repozitáře.

Jelikož v repozitářích zpracovávám vše (vytvoření/aktualizace/mazání entity) tak občas potřebuju injektnout jeden repozitář do druhýho. A extenze proto, abych pořád nemusel v configu psát setup: [injectRepos1, injectRepos2, ...].

Do teď všechno funguje super. Problém ale nastane, pokud vytvořím kruhovou závislost mezi repozitářema. Pak vyskakuje „nečekaná“ vyjímka Circular reference detected for services. Je to logický protože se setup() volá hned při vytvoření instance první třídy, ale v té době ještě není vytvořena instance druhé třídy, která se má injektnout do té první. A tak se začne vytvářet, ale chce instanci první třídy, aby si ji mohla injektnout k sobě, ale ta ještě není hotová… Je to jasný proč to dělá. Otázka z ní: jde z toho nějak ven? Ideálně rozšířením extenze:

protected function createServiceRepos1()
{
	$service = new Repository1(...);
	if (!$service instanceof Repository1) {
			throw new Nette\UnexpectedValueException('...');
	}

	//>> TADY neco udelat aby se prave vytvorena sluzba pridala do stromu,
	// aby volani $this->getService('repos1') nehodilo vyjimku.

	$service->injectRepository2($this->getService('repos2'));
	return $service;
}

Co sem koukal, tak to asi moc nepůjde, protože si to kontejner hlídá, takže volání addService() nepomůže.

Jediný rozumný na co sem přišel by bylo nevolat addSetup(), ale přesunout to až do afterCompile() do metody initialize(). To je ovšem problém, protože sice budu mít „cyklický“ repozitáře, ale za cenu toho, že se mi všechny vytvoří najednou, takže nejen že to nebude lazy, bude to megaNElazy :-(

Nedá se to tedy pořešit nějak inteligentně?

Jan Tvrdík
Nette guru | 2595
+
0
-

@hAssassin: Vidím dvě cesty, jak z toho ven:

  1. Akademická – uprav si model, aby neměl kruhové závislosti
  2. Pragmatická – vytvoř si service locator a injektni ho do všech repozitářů (při injektnutí se repozitář může automaticky do service locatoru zaregistrovat)
hAssassin
Člen | 293
+
0
-

@Jan Tvrdík > ahoj, díky za odpověď. Model bez kruhových závislotí by byl sice pěkný, ale momentálně to nevidím reálně. Ta druhá možnost je lepší. Ale nevím jestli to chápu přesně:

  1. Byl by service locator, který by se předával do všech repozitářů třeba v konstruktoru a hnedle by se do něho repozitář registroval.
  2. Pokud byl bylo potřeba pracovat s jiným repozitářem, tahal by se ze service locatoru.
  3. Úplně by tedy z repozitářů zmizely inject* metody.
<?php

class Repository1 {
	protected $dbconn;
	protected $locator;

	public function __construct($dbconn, $locator) {
		$this->dbconn = $dbconn;
		$this->locator = $locator->add($this);
	}

	public function getSomethingFromAnotherRepos() {
		return $this->locator['anotherRepos']->something();
	}

Chápu to správně? Pokud ano, tak je to zajímavá myšlenka, a tady by asi měl service locator svoje opodstatnění.

Každopádně jsem to prozatím vyřešil tak, že jsem kritickým službám přidal tag a v afterCompile() projdu právě všechny služby, které tento tag mají a v initialize() jim injektnu poźadovaný repos. Problém z natáhováním všech služeb jsem omezil pouze na ty, který mají ten tag (ona ta kruhová závislost určitě bude jen u nějakých a bude jich menší množina, takže zatím snad OK, ale uvidím).

hAssassin
Člen | 293
+
0
-

Jinak ješte abych to uvedl na konkrétním případě. Mám produkty a kategorie. Přičemž vazba mezi nimi je m:n. Mám tedy ProductRepository a CategoryRepository a chtěl bych volat nad ProductRepository metodu getCategories($productId) a současně nad CategoryRepository volat getProducts($catId).

Současně si ale myslím, že by nebylo správný hrabat v ProductMapperu do tabulky category ani v CategoryMapperu do tabulky product. Mapper at si pracuje pouze nad svojí tabulkou, resp. několika tabulkami, které tvoří logický celek (což zde už neplatí).

Pak ale potřebuju mít kruhovou závislost…

Jan Tvrdík
Nette guru | 2595
+
0
-

Chápu to správně?

Ano, a už vím, kdy to nebude fungovat :) Když si třeba do presenteru injektneš ProductRepository, tak dokud nebude vytvořena instance CategoryRepository, tak ty metody, co ho potřebují nebudou fungovat.

hAssassin
Člen | 293
+
0
-

heh :) shit… A co si myslíš o tom tagování a injektování v initialize() přes afterCompile() ?

enumag
Člen | 2118
+
0
-

@hAssassin: TL;DR… Osobně preferuji způsob, že Repository pracují pouze s jednou tabulkou a jakékoli metody vyšší úrovně, které potřebují pracovat s více tabulkami, dávám do Facade. Takto cyklus mezi repozitáři nevznikne.

castamir
Člen | 629
+
0
-

@enumag já bych to zobecnil na skupinu tabulek pod jedním repozitářem, v případě, že ty tabulky jsou úzce svázány a navenek je není potřeba rozlišovat (viz třeba Closure Tables). Ale jinak to dělám úplně stejně tj. přes fasádu.

enumag
Člen | 2118
+
0
-

@castamir: Souhlas. I když bych možná řešil jako další službu vedle, která by využívala několik repository a sama by byla využívána fasádami, ale to už je detail a záleží na konkrétním případě.

Editoval enumag (20. 4. 2013 11:11)

hAssassin
Člen | 293
+
0
-

@enumag > díky za odpověď. Ano, nad tím sem taky uvažoval, ale nejsem si jistý jestli by to vlastně vubec něco řešilo, protoze by se ta kruhová závislost přenesla jen z repozitářů do fasád. Nebo si teď nejsem jistý jak to přesně myslíš:

  1. fasáda přímo pracuje z několika repositáři, které se pouze injektnou do ní při jejím vytvoření, nebo
  2. fasáda pracuje s ostatními fasádami a přes ně přistupuje až k repozitáři (čili injektují se zase fasády).

Předpokládám, že myslíš asi a), protože b) vlastně nic neřeší?

enumag
Člen | 2118
+
0
-

@hAssassin: Ano, měl jsem na mysli a). Pokud by to znamenalo duplikování kódu v několika fasádách, vytvořil bych ještě další službu stranou, jak uvádím ve svém předchozím příspěvku.

hAssassin
Člen | 293
+
0
-

@enumag > ano chápu a asi to nejčistčí řešení jak z toho ven. Já mám problém ten, že chci mít možnost přes entitu přitupovat k jiným entitám přes cizí klíče. Čili když se entita (např produkt) inicializuje, nastaví si categoryId na id daný kategorie (za předpokladu, že je tam vazba 1:n). A pokud někde (třeba až v šabloně) zavolám $entity->getCategory() natáhne a vrátí mi to právě danou kategorii (pokud existuje). K tomu sice musí být entita závislá na repozitáři, což mi zase tak nevadí, ale repozitář (produkt) je pak závislí na jiném (category).

Možnost, že by entita byla závislá místo toho nebo současne přímo na facádě, mi nepříjde jako zrovna nejlepší.

enumag
Člen | 2118
+
0
-

V případě YetORM entita na získání těch souvisejících dat z jiné tabulky imho nepotřebuje repository (imho už to by bylo špatně). Pokud je získávání souvisejících dat složitější a jsou potřeba cizí repository, nemělo by to být implementované přímo v entitě, ale pouze v Facade.

Editoval enumag (20. 4. 2013 15:12)

hAssassin
Člen | 293
+
0
-

Aha, YetORM presně neznám, každopádně to přímo v entitě nemám, ta metoda getCategories() je pouze zkratka, uvnitř se volá repozitář pro danou entitu a v něm ses volá repozitář cizí, proto ho tam potřebuju. Představa, že bych z repozitáře nebo z entity volal fasádu a ta by volala cizí repo se mi nelíbí a nelíbí se mi ani fakt, že bych o tuto funkcionalitu přisel :-)

enumag
Člen | 2118
+
0
-

Osobně mám problém už s tím repository v entitě. :-) O používání repository v jiném repository nebo fasády kdekoli v nižších vrstvách ani nemluvě. Přesto v YetORM tu metodu getCategories lze snadno implementovat.

hAssassin
Člen | 293
+
0
-

Jo, taky mi to nedělá úplně dobře… :-) Jak jsem psal, YetORM moc neznám, prakticky vůbec, mohl bys nastínit jak tu metodu implementovat?

enumag
Člen | 2118
+
0
-

Z hlavy ne, ještě jsem neměl čas vyzkoušet YetORM v praxi. Nějak se vytvoří kolekce entit které předáš data z volání ActiveRow::related.

castamir
Člen | 629
+
0
-

Napadá mě určitá struktura modelu (zatím opravdu jen nápad): Mějme nějaký modelContainer (něco jako service locator), který bude zastřešovat všechny repository a k nim příslušné entity a mappery. Zároveň všechny entity budou striktně propojené s repozitářem např. nějak takto:

$entity = $repository->createEntity();
//nebo
$entity = new Entity();
$repository->attach($entity);

pak požadavek volaní metod ekvivalentních metodám ref resp. related z NDB by probublal z dané entity/kolekce přes repozitář do modelContainer, kde by se zavolal patřičný repozitář, který by to zpracoval a vrátil. A teď se do mě pusťte :D

Edit: vyřešilo by to např. teoreticky neomezené odkazování přes cizí klíč.

Editoval castamir (20. 4. 2013 16:36)

enumag
Člen | 2118
+
0
-

@castamir: Nějak mám pocit že popisuješ doktríní entityManager. :-D (Ale mohu se mýlit, už je to hodně dlouho co jsem naposledy používal doktrínu.)

Editoval enumag (20. 4. 2013 16:43)

hAssassin
Člen | 293
+
0
-

@castamir > ano, o něco takového se pokouším, akorát že bez service lokatoru. Tady by se však celkem mohl hodit. A ano, chci mimojiné dosáhnout nekonečné odkazování přes cizí klíč :-)

Každopádně mě teda ještě napadlo povolit možnost, aby fasáda mohla přistupovat k více repozitářům, tak jak psal enumag, potom sama fasáda musí všechny tyto repozitáře vyžadovat a ty pak sama rozstrkat do repozitářů dál. Např. ProductFacade vyžaduje ProductRepos a CategoryRepos, ty se do ní injektnou a ona pak ví že CategoryRepos musí předat do ProductRepos a ProductRepos do CategoryRepos. Címž se sice kruhová závislost vytvoří, ale až po tom, co jsou inicilizovaný všechny potřebný repozitáře.

Má to dvě nevýhody:

  1. porušuje to SRP, protože fasáda částečně přebírá činnost kontejneru/service locatoru,
  2. porušuje to DRY, protože teoreticky bude potřeba totéž udělat i v CategoryFacade (stejný kód, šlo by to ale asi pořešit automaticky už v CompilerExtension).

Co myslíte?

enumag
Člen | 2118
+
0
-

@hAssassin: Co je na tom špatně sis vysvětlil sám víc než dostatečně. Neber to špatně, to odkazování přes cizí klíč je lákavá věc, ale za tuhle cenu bych do toho nešel.

Editoval enumag (26. 1. 2014 17:12)

David Grudl
Nette Core | 8229
+
0
-

Konfigurační soubor by neměl nahrazovat programování. Takže úvaha by se měla odvíjet od toho, jak bych stejný problém řešit čistě v PHP. No a vyrobil bych si na to továrničku.

Tedy místo původního

services:
	a: ClassA
	b: ClassB

kde potřebujeme objekty @a a @b nějak propojit, si vytvořím továrnu:

class ABFactory
{
	function __construct(A $a, B $b)
	{
		// zašmodrchám je
		$a->setB($this->b);
		$b->setA($this->a);

		$this->a = $a;
		$this->b = $b;
	}

	function getA() {
		return $this->a;
	}

	function getB() {
		return $this->b;
	}
}

A využiju jejích služeb:

services:
	abFactory: ABFactory(ClassA, ClassB)
	a: @abFactory::getA
	b: @abFactory::getB
hAssassin
Člen | 293
+
0
-

@David Grudl > super díky, já věděl že to nějako půjde a zrovna řešení přes továnu jsem tipoval jako možný, jen mě nenapadlo, jak to napojit do neonu. Ale ještě jedna (asi hodně blbá) otázka: půjde to nějak zautomatizovat?

David Grudl
Nette Core | 8229
+
0
-

Co bys na tom chtěl automatizovat?

hAssassin
Člen | 293
+
0
-

No, jak sem říkal, blbý dotaz a vlastně to odporuje tomu, co jsi psal hned v úvodu svého prvního příspěvku. Takže beru zpět.

Jinak sem měl na mysli automatizovat ono vytváření, případně konfiguraci těch továrniček např. v rozšíření (což by asi šlo). A úplně ideálně i jejich kód (což už zavání příliš velkou leností :-).

miler
Člen | 75
+
0
-

Snažím se implementovat Davidovo řešení, ale dostávám se do fáze "Reference to missing service @abFactory::getA", v jaké verzi Nette tenhle postup funguje, prosím? Jsem s aktuálním 2.0 v pytli?

Jak bych pak prosím nahradil službu definovanou takto?

a:
		class: A
		setup:
			- setXY(%xy%)

Edit:
Možná je to tím že neumím továrničce předat ty původní modely?

testFactory: Models\TestFactory(Models\A, Models\B)

mi vygeneruje

/**
* @return Models\TestFactory
*/
protected function createServiceTestFactory()
{
	$service = new Models\TestFactory('Models\\A', 'Models\\B'');
	return $service;
}

Ale ostatní služby, které dřív vyžadovaly službu A mi po přepsání a na:

a:
	@testFactory::getA

hlásí že služba typu Models\A neexistuje a že mám zkontrolovat typehint.

TestFactory vypadá takto:

<?php
namespace Models;

class TestFactory {

    public function __construct(\Models\A $a, \Models\B $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function getA() {
        return $this->a;
    }

    public static function getB() {
        return $this->b;
    }

}

Editoval miler (22. 10. 2013 11:17)

miler
Člen | 75
+
0
-

Přidal jsem těm getterům ve factory return type do phpdoc komentáře a posunul jsem se k:

Argument 1 passed to Models\TestFactory::__construct() must be an instance of Models\A,
string given...

tzn. že asi opravdu „jen“ neumím předat správně ty modely jako parametry pro továrničku?

Díky


Edit:

Ty parametry tam vlastně možná nejsou vůbec potřeba? Nicméně teď se dostanu do dalšího kroku, kde vymrznu na statickém volání getA() z configu.

Non-static method Models\TestFactory::getA() should not be called statically,
assuming $this from incompatible context

Edit 2:

Tak tam asi potřeba jsou, jinak se mi nevytvoří žádná služba pro TestFactory.

Už jsem se zamotal a nevím kudy kam :-)

Editoval miler (23. 10. 2013 12:42)

miler
Člen | 75
+
0
-

Nedokázal by mi prosím někdo zkušenější poradit jak se z této šlamastiky vymotat? Už jsem prošel forum 4× tam a zpět, vyzkoušel spousty tipů a triků ale ne a ne s tím hnout. Díky moc.

Dočasně jsem to vyřešil tak že jsem ty metody co potřebují jiný model dal do jiného spolčného modelu.

Editoval miler (24. 10. 2013 12:18)

Filip Procházka
Moderator | 4668
+
0
-
services:
	testFactory: Models\TestFactory(Models\A(), Models\B())