Dependency Injection ve 2.1: Anotace @inject

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

David Grudl napsal(a):

  • Jakékoliv hinty v kódu pro DI kontejner, např. anotace, jsou zlo. (sic!)

Takovým hintem je i ostatně konvence pojmenování metod tak, že začínají na inject ;)

Ale pokud se s objektem dá i přes ohintování normálně pracovat, tak jsem ochotný to z praktických důvodů tolerovat.

pekelnik
Člen | 462
+
0
-

@**Filip Procházka**: koukal ses na ten „kód“:„https://github.com/rosret/rfc“? Pokud někomu nevoní reflexe může napsat settery… například write-once…

Editoval pekelnik (3. 5. 2013 14:59)

Filip Procházka
Moderator | 4668
+
0
-

duke napsal(a):

A co má být, že to není pomocí jazykových konstruktů? Komu to vadí a proč? Pokud bys byl v tomto přístupu důsledný, musel bys úplně zakázat používání anotací.

Například mně to vadí, protože to není čistý způsob práce s objekty. OOP === posílání zpráv veřejným api objektu (metodami).

Anotace jsou metadata. Jenom je nemám v externím neon souboru ale u toho prvku, kterého se týkají. Takový zápis metadat (externí soubor vs phpdoc) je zcela ekvivalentní, je to jenom otázka preferencí. Viz doctrine.

Pak jsou tu ale také anotace, které řídí (typu @inject nebo @service(nazev-sluzby)) a ty už úplně čisté nejsou, protože se s nimi porušuje inversion of control, pokud na ně container spoléhá a třída spoléhá na to, že container o nich ví.

Pokud je tam opravdu slabá vazba (@inject na public property) tak je to (pro mě) tolerovatelný hint.

pekelnik napsal(a):

koukal ses na ten kód? Pokud někomu nevoní reflexe může napsat settery… například write-once…

To jsi tam ale přidal před pár minutami ;) Ovšem když setter nenapíšu, tak mi to stejně tu instanci vnutí. Imho je dost wtf chování. Ale zajímavý nápad :)

Editoval Filip Procházka (3. 5. 2013 15:06)

David Grudl
Nette Core | 8227
+
0
-

ad public vs private property:

Během celého života objektu je záhodno, aby property, ve které je uložena služba, byla private:

  • nikdo ji nemůže zrušit nebo změnit za jiný objekt (ani omylem)
  • nikdo k ní nemůže přistupovat (závislosti nejsou součástí veřejného API, nikdo s nimi nepočítá)
  • patří jen třídě (její dostupnost potomkům via protected je už diskutabilní)

Existuje jediný okamžik, kdy je private překážkou:

  • zápis do property zvenčí

Tomuto okamžiku se dá vyhnout extrémně snadno a zcela čistým způsobem:

  • do property bude zapisovat metoda (konstruktor nebo setter, který se klidně může jmenovat inject)

Až sem není problém. Problém nastává v momentě, kdy jsme líní setter psát. Tohle třeba zdůraznit: řešíme jen syntactic sugar pro líné programátory. A zdá se, že žádné čisté řešení neexistuje. Zcela chápu, že každý vidí jakožto nepřekročitelnou jinou nevýhodu. Pravdu mají všichni.

Asi jediné čisté řešení: skriptík, který na základě @inject anotací u private property vygeneruje (přegeneruje) konstruktor či setter. Kam nemůže jazyk, tam nasadíme transpiler ;)

Otázka pak je, jestli by se taková věc mohla dít i dynamicky nebo on-the-fly (mezivrstvou ala RobotLoader). Technicky vzato dala, a to už jsme vlastně kousek od injektování do private property, problém ovšem je, že se závislosti vytratí ze „statického“ veřejného API. Tedy třeba toho API, které generuje Apigen nebo zobrazuje code completion ve spoustě editorů.

Druhé v podstatě čisté řešení: použít public property a rezignovat na zapouzdření, za podmínky, že s objektem se nikdy přímo nepracuje (tj. zapouzdření nemůže být třeba omylem porušeno). Této velmi specifické situaci vyhovují právě presentery. (Zdůrazňuji, že nejde o řešení pro presentery, ale pro typ objektů, do nějž presentery pouze spadají).

V případě presenterů se dá namítnout, že je ani neinstancujeme, tudíž můžeme rezignovat na nemožnost zápisu do private property a povolit injektování do nich. To je pravda, ale když už existuje pro presentery čistější nedokonalé řešení s public property, je zbytečné implementovat i to méně šťastné.

U obou typů property injektáže se dostaneme k jinému problém: pro svou jednoduchost budou používány i mimo specifické oblasti, kde je lze tolerovat. Což je vlastně diskriminuje asi nejvíc.

David Grudl
Nette Core | 8227
+
0
-

Honza Marek napsal(a):

Takovým hintem je i ostatně konvence pojmenování metod tak, že začínají na inject ;)

Skutečně to tak vypadá, nicméně tahle věc vznikla absolutně bez ohledu na kontejner. Inject označuje, že jde o „konstruktor“ injection.

Tharos
Člen | 1030
+
0
-

@dg: Výborně, diskuze se konečně zase posunula, díky. :)

Napadlo mě ještě jedno řešení, a totiž, že by injektování v kontejneru bylo snadno upravitelné. Například tak, že kontejner udržoval nějaký injector, který by ve výchozí implementaci injektoval přes konstruktor a přes inject metody, ale který by si každý snadno mohl upravit (a kompozicí kontejneru předat) tak, aby injektoval třeba i do public či private property s nějakou anotací (podle chuti).

Viděl bych to tak, že tenhle hacknutý injector by vůbec nemusel být nativní součástí Nette, takže by Nette nikoho nevedlo k prasáčení (nebo jaký je pro to termín, já se v tom vážně neorientuju). Důležité je, že programátor, který si je schopen napsat takový injector pracující s reflexí, není vyloženě začátečník a je sto vyhodnotit výhody a nevýhody zvoleného řešení ve svém kontextu.

Pokud by to z nějakého důvodu nešlo kompozicí, asi by mělo stačit v kontejneru injektování vyčlenit do nějaké samostatné metody, kterou by pak bylo možné přepsat v poděděném kontejneru a ten by se v aplikaci použil.

Co si myslíte o takovémto řešení?

Editoval Tharos (3. 5. 2013 15:33)

pekelnik
Člen | 462
+
0
-

@**David Grudl**: transcompilátor je také zajímavý nápad… ovšem jakožto ne-uživatel robot loaderu bych nebyl nadšený kdyby to na něm záviselo…

Co si myslíš o použití properties z Nette\Object?

class A {}
class B extends Nette\Object {
	/** @inject @var A */
	private $bar;
	public function setBar(A $bar) { $this->bar = $bar; }
}

$foo = new B();
$foo->bar = new A(); // <- injection here

@**Tharos**: vyměnitelný injector se mi také líbí. Ještě zajímavější mi přijde možnost pouze aktivovat PrivatePropertyInjector a injektovat foreach $injectors as ...

Editoval pekelnik (3. 5. 2013 15:42)

Tharos
Člen | 1030
+
0
-

pekelnik napsal(a):
Ještě zajímavější mi přijde možnost pouze aktivovat PrivatePropertyInjector a injektovat foreach $injectors as ...

No já bych to samozřejmě taky ocenil :), ale na to mnozí namítnou, že tím „Nette nabádá k prasněčení, voe!“. Klidně bych si z toho důvodu ten injector napsal sám.

David Grudl
Nette Core | 8227
+
0
-

Vlastní injektovač lze (nenativně) použít tak, že napíšete:

services:
	myService:
		class: Abc
		setup:
			- PrivatePropertyInjector::inject(@self)
Tharos
Člen | 1030
+
0
-

@dg: No! Tak už jenom doplnit možnost nastavit nějaký výchozí setup „globálně“ pro všechny služby, aby to bylo kompatibilní i s kratšími zápisy, a já osobně budu naprosto spokojenej. :)

David Grudl
Nette Core | 8227
+
0
-

Ještě navážu k předchozí poznámce, že nevýhoda private property injection je v tom, že závislosti nenajdeme v API nebo je nezobrazí code completion.

Nutno totiž dodat, že nevýhoda public property je v tom, že je v API naopak najdeme a že je code completion zobrazí ;-)

Protože public property s anotací @inject jsou write-only, navíc i write-once-only.

Vůbec nevidět závislosti nebo vidět je jako public property – obojí je zavádějící. Protože PHP dosud žádné write-only property nepodporuje, vrací nás to zpátky k setteru. A ten se nám prostě nechce psát.

Jako uspokojivé řešení by mohla přijít anotace @method, kterou tu už zmínil Šaman, nebo také anotace @property-write. To by mohl Nette\Object i nativně podporovat.

Problém s private je ten, že je spjat s třídou, tudíž díky dědičnosti může vzniknout více stejně pojmenovaných properties. Aby bylo jasné, co $obj->abc = $abc znamená, musela by se používat viditelnost protected. Výhodou pak je možnost nastavovat takové properties i bez reflection (tj. myslím uvnitř Nette\Object, nemluvím o DI kontejneru).

Takže místo současného

class Foo extends Object
{
   /** @var A @inject */
   public $a;

   /** @var B @inject */
   public $b;
}

by existovalo například něco jako

/**
 * @method void injectFoo(A $a, B $b)
 */
class Foo extends Object
{
   /** @var A */
   protected $a;

   /** @var B */
   protected $b;
}

Nebo

/**
 * @property-write $a @inject
 * @property-write $b @inject
 */
class Foo extends Object
{
   /** @var A */
   protected $a;

   /** @var B */
   protected $b;
}

Což je ve snadnosti někde mezi psaním funkce a anotací @inject

Tharos
Člen | 1030
+
0
-

@dg: Nastíněná řešení se mně osobně líbí určitě více, než public property injection. :)

A co bys řekl třeba na něco podobného? Je to nástřel, kterému chybí milion věcí, ale myšlenka je z něj myslím dobře patrná:

use Nette\Reflection\ClassType;

abstract class Injectable
{

	public function __call($name, array $arguments)
	{
		if ($name === 'inject') {
			$services = array();
			foreach ($arguments as $service) {
				$services[get_class($service)] = $service;
			}
			$reflection = new ClassType(get_called_class());
			foreach ($reflection->getProperties() as $property) {
				if ($property->hasAnnotation('inject')) {
					$type = $property->getAnnotation('var');
					if (isset($services[$type])) {
						$this->{$property->getName()} = $services[$type];
					}
				}
			}
		}
	}

}

class A {};
class B {};

class C extends Injectable
{

	/**
	 * @var B
	 * @inject
	 */
	protected $a;

	/**
	 * @var A
	 * @inject
	 */
	protected $b;

	public function isProperlyInitialized()
	{
		return $this->a !== null and $this->b !== null;
	}

}

$a = new A;
$b = new B;

$c = new C;
$c->inject($a, $b);

$c->isProperlyInitialized(); // returns true

Samozřejmě s tím, že magickou metodu inject by volal automaticky kontejner. Plus Ti, co chtějí mít navenek deklarované závislosti, si mohou ke třídě dopsat například tu @method void inject(...) anotaci.

Třeba s Tebou navrženou anotací @method void injectFoo(A $a, B $b) je na můj vkus pořád moc psaní (já už se za tu lenost fakt stydím), protože typ píšu dvakrát, název proměnné dvakrát…


Plus jsem se Tě chtěl zeptat, jestli se Ti obecně líbí myšlenka nějakých extenzí samotného kontejneru, které by dokázaly doladit, jak kontejner injektuje?

Díky za utišení toho flame tady! :)

Editoval Tharos (3. 5. 2013 21:35)

Filip Procházka
Moderator | 4668
+
0
-

@Tharos: Takže v podstatě přesně to co jsem vytvořil já :)

mishak
Člen | 94
+
0
-

@Tharos Pokud bude všechno v anotacích je třeba automatizovat i validaci.

Ďábelská myšlenka, co nepodporovat NULL a nutit tak programátora psát NullObjekty? Došlo by k redukci stavů a nevědomky by lidi psaly míň podmínek v kódu. (Až na ty co by měly špatný návrh/neuměly navrhnout NullObjekt.)

@Tharos Místo isProperlyInitialized je zavádějící, protože podmiňuje stav isUnproperlyInitialized, které neexistuje (nelze takového stavu z principu dosáhnout, inicializace buď proběhne pořádně nebo dojde k vyjímce nebo neproběhne vůbec).

Bych byl pro jeden z těchto názvů:
$object->isJackedOnStimpacks();
$object->isVaccinated();
nebo fádní $object->hasBeenInjected();
nejlépe i s opačnou funkcí hasNotBeenInjected.

Editoval mishak (6. 5. 2013 16:31)

maryo
Člen | 15
+
0
-

Když se tu hodně píše o reflexi, tenhle „hack“ znáte?

<?php
class A
{
    private $a, $b, $c;
}

$a = new A;
$inject = function() {
    $this->a = 'a';
    $this->b = 'b';
    unset($this->c); // tohle "zvenku" asi ani jinak nejde, minimálně ne přes reflexi
};
$injectToA = $inject->bindTo($a, $a);
$injectToA();

var_dump($a);
?>

Nebo taky tohle je „vtipný“. Ale to už jsem dost OT.

<?php
class A
{
    private $a, $b, $c;
}

$a = new A;
array_walk($a, function(&$value) {
    $value = 'test';
});

var_dump($a);
?>

Editoval maryo (10. 5. 2013 0:07)

David Grudl
Nette Core | 8227
+
0
-

Známe, ale bohužel je třeba držet kompatibilitu s PHP 5.3.

bene
Člen | 82
+
0
-

Navrhuji používat jako preferovaný typ injektování konstruktor a právě anotace @inject. Naopak bych zcela (v příkladech, dokumentaci) opustil metody inject–. Ty vznikly čistě jako workaround kvůli problémům s konstruktorem a dědičností a jejich použití jinde je nešťastné. A protože ho vídám čím dál tím častěji, aby by bylo vhodné mu udělat přítrž.

Obávám se, že to stejné se stane i s public property, private property, apod. :-(

Pokud můžu mluvit za sebe, tak mi u presenterů public property pro injektování závislostí vůbec nevadí. Naopak je uvítám.

A i když jsem co se čistoty kódu docela purista přiznám se, že public property pro předání závislostí již delší dobu využívám například u továren formulářů. Ty v podstatě jen vytvářejí formulář a implementují metodu pro zachycení submittu. Některé formuláře prostě vyžadují i 4 a více závislostí a public property je kratší než construct a přiřazení (i když to jde vygenerovat), který pak vypadá děsně a tvorba instance vypadá ještě děsněji :-) Možná nutno podtknout, že to takto řeším v Nette 0.9.x.

Šaman
Člen | 2662
+
0
-

Tak kromě presenterů využívám inject metody i v komponentách. Je to z toho důvodu, že v základu mají dva nepovinné parametry, se kterýma si neporadí autowiring.
Když je skryju, tak je to v pohodě, dokud nechci třeba v BaseControl injectovat nějakou službu (u mě templateLocator). Vpodstatě je situace stejná jako u presenterů (moje komponenty mají už v základu tři parametry – $parent, $name a $templateLocator). Ten mi ale dělá z nepovinných parametrů povinné a je problém při vytváření.

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

Proč nepoužíváš u komponent k předání závislostí konstruktor, který tě zbaví právě těch 2 nepovinných závislostí v základu?

Šaman
Člen | 2662
+
0
-

Těch dvou nepovinných parametrů jsem se zbavil, ale $templateLocator mi tam dost vadí. U každé komponenty, které chci injectovat nějakou závislost ho musím uvést. Přitom je to spíš taková ‚technologická‘ záležitost, která nesouvisí s logikou modelu.

<?php

/**
 * Abstraktní komponenta, která si umí dohledat šablonu
 */
abstract class BaseControl extends Nette\Application\UI\Control
{
	/** @var Annivers\Services\TemplateLocator */
	protected $templateLocator;


	/**
	 * V konstruktoru skryjeme nepovinné parametry kvůli DI kontejneru
	 */
	public function __construct(Annivers\Services\TemplateLocator $templateLocator)
	{
		parent::__construct();
		$this->templateLocator = $templateLocator;
	}
.
.

?>
<?php

/**
 * Komponenta události
 */
class EventControl extends BaseControl
{

	/** @var Annivers\Model\Data\EventRepository */
    protected $eventRepository;


    public function __construct(Annivers\Services\TemplateLocator $templateLocator, Annivers\Model\Data\EventRepository $eventRepository)
	{
		parent::__construct($templateLocator);
		$this->eventRepository = $eventRepository;
    }
.
.

?>

Raději bych $templateLocator injectoval pomocí metody a nechal konstruktor prázdný pro logické vazby. Přemýšlím o tom, že tuto věc vyřeším tím, že šablonu bude hledat přímo metoda v abstraktní komponentě, ale obecné řešení to není.

Editoval Šaman (5. 6. 2013 19:56)

Tomáš Votruba
Moderator | 1114
+
+1
-

Kdysi tu padlo

/** @var Cache|NULL */
public $cache;

pro případ, kdy služba neexistuje. Je to již někde implementované, nebo je stále potřeba to takto obcházet?

/** @var Cache */
private $cache;

public function inject(Cache $cache = NULL)
{
	$this->cache = $cache;
}