Podpora pro automatické injectnutí kruhových závislostí služeb v Containeru

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

Narazil jsme teď v jednom projektu na problém kruhových závislostí služeb. Jako doporučované řešení jsem na fóru našel tohle:

https://forum.nette.org/…-sluzeb-v-di#…

Takovou továrničku samozřejmě není problém si napsat, ale napadlo mě proč to nedělá přímo Container automaticky?

Jak by to mohlo fungovat:

  • createService vy se rozdělilo na dvě části constructService a initService
  • constructService by pouze vytvořila objekt service a předala závislosti v konstruktoru
  • initService by dělala zbytek – zavolání setup, inject závislostí do metod a property
  • pokud bych si vyžádal službu z Containeru tak by se nejdříve zavolala constructService, ta by mohla vyžadovat pro závislosti konstruktoru získání dalších služeb, ale u všech by se volalo pouze constructService
  • pokud by vznikla kruhová závislost v závislostech konstruktoru tak by to stejně jako teď hodilo exception
  • pokud ne tak by se postupně vytvořily všechny služby potřebné pro constructor injection
  • teprve potom by se zavolaly pro všechny tyto služby jejich metody initService, které by provedly dodatečné injectnutí. Tady už nám kruhové závislosti nevadí, protože služby už existují. Jen je musíme pospojovat.

Je nějaký důvod proč to Nette takto nedělá? Neměl by asi být takový problém to doplnit. Když tak můžu připravit pull-request. Jen sem se chtěl ujistit, že tohle chování není by-design.

Editoval mystik (27. 3. 2014 14:54)

David Matějka
Moderator | 6445
+
0
-

Zajimavy, presne nad timhle chovanim jsem premyslel vcera a napadlo me stejny reseni. Napadlo me par technickych problemu, ktery by se snad nechaly vyresit, je tam vsak jeden pomerne zasadni problem: nejaka sluzba muze (i kdyz by nemela) v konstruktoru spolehat na to, ze zavislost, kterou uz obdrzela, je plne funkcni a inicializovana.

Filip Procházka
Moderator | 4668
+
0
-

Vůbec se mi nelíbí jak jsi to navrhl, celý container se strašlivě zkomplikuje.

Pokud máš takové závislosti v aplikaci, může to být signálem že máš blbě navrženou aplikaci.

mystik
Člen | 291
+
0
-

@matej21: To mě taky napadlo, ale v takovém případě už to stejně nejde nijak automatizovaně vyřešit.

@Filip: Jestli ty závislosti nejsou chyba návrhu sem přemýšlel jako první. Ale nenapadl mě způsob jak se jich zbavit aniž by to podstatně zkomplikovalo aplikaci.

Myslím, že Container potom o tolik komplikovanější nebude, pokud sem tedy něco nepřehlídl. Rozdělení createService* na dvě části je jen minimum kódu navíc. Jediné co by se změnilo dál by bylo, že v $this->creating by se seznam vytvářených služeb držel až do chvíle, kdy se série volání vrátí do první úrovně creteService(), místo aby se odtud service odstranila hned. A po návratu na první úroveň by se pak nad všemi service v $this->creating v cyklu zavolaly ty initService().

David Matějka
Moderator | 6445
+
0
-

Myslim, ze takovyhle univerzalni reseni by toho vic rozbilo nez spravilo. Myslim, ze DIC si nemuze dovolit predavat zavislosti, ktere nejsou plne inicializovane.. Maximalne pokud by to bylo implementovane tak, ze by container builder dokazal ve fazi kompilace rozpoznat kruhove zavislosti a tohle reseni umel aplikovat jen na dotcene sluzby.

Ale kruhova zavislost se ve vetsine pripadech necha odstranit snadno, takhle by to svadelo ke psani spatne navrzenych aplikaci :) vcera jsem se teda taky dostal do stadia, kdy to neslo odstranit. Nakonec jsem to vyresil proxy tridou s accessorem na dotcenou sluzbu.

EDIT: kdyz uz jsem u toho, tak rovnou napisu reseni, kdyby nekdo potreboval

interface IFoo
{
	public function doFoo();
}

interface IBar
{
	public function doBar();
}

class FooImplementation implements IFoo
{
	protected $bar;

	public function __construct(IBar $bar)
	{
		$this->bar = $bar;
	}

	public function doFoo()
	{
....
	}
}

class BarImplementation implements IBar
{

	protected $foo;

	public function __construct(IFoo $foo)
	{
		$this->foo = $foo;
	}

	public function doBar()
	{
....
	}
}
services:
	- FooImplementation
	- BarImplementation

Ted nam tam vznikla kruhova zavislost, vytvorim si nette generovany accessor pro FooImplementation

interface FooAccessor
{
	/**
	* @return IFoo
	*/
	public function get();
}

a proxy tridu

class FooProxy implements IFoo
{
	protected $fooAccessor;

	public function __construct(FooAccessor $accessor)
	{
		$this->accessor = $accessor;
	}

	public function doFoo()
	{
		return $this->fooAccessor->get()->doFoo();
	}
}

a neon upravime:

services:
	fooImplementation:
		class: FooImplementation
		autowired: false
	-
		implement: FooAccessor
		create: @fooImplementation
	- FooProxy
	- BarImplementation

Editoval matej21 (27. 3. 2014 16:06)

mkoubik
Člen | 728
+
0
-

Tip: kruhové závislosti se často dají dobře rozseknout pomocí událostí (s klasickým event managerem, nikoliv observer pattern z Nette\Object).

mkoubik
Člen | 728
+
0
-

@matej21: ten FooAccessor ti ale pokaždé vytváří novou instanci, ne?

David Matějka
Moderator | 6445
+
0
-

@mkoubik: nn factory vytvari, accessor vytahne z containeru tu stejnou instanci

mkoubik
Člen | 728
+
0
-

Wow, teď na to koukám do ContainerBuilderu, jaktože jsem o tomhle nevěděl?

Filip Procházka
Moderator | 4668
+
0
-

@mkoubik protože to není zdokumentované :)

mystik
Člen | 291
+
0
-

@matej21: No další možnost by byla tohle aplikovat jen na základě nějaké konfigurace.

services:
  fooService:
    class: \FooService
    lazyInit: true

Container by takhle označené služby inicializoval až odloženě jak jsme navrhoval. U ostatních služeb by to ale šlo stejně jako dosud. Pokud si něco takhle označím tak beru na vědomí že to ostatní service můžou do konstruktoru dostat neinicializované.

@mkoubik: Nejde o to že by to rozseknout nešlo, ale dost mi to zbytečně zkomplikuje aplikaci. Proto hledám jednodušší řešení.

Editoval mystik (27. 3. 2014 17:43)

mystik
Člen | 291
+
0
-

Nakonec tedy vyřešeno přes vlastní CompilerExtension, které zařídí korektní vytvoření a pospojování všech služeb.