Dependency Injection ve 2.1: Anotace @inject
- Honza Marek
- Člen | 1664
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
@**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
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
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
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
@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
@**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
pekelnik napsal(a):
Ještě zajímavější mi přijde možnost pouze aktivovatPrivatePropertyInjector
a injektovatforeach $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
Vlastní injektovač lze (nenativně) použít tak, že napíšete:
services:
myService:
class: Abc
setup:
- PrivatePropertyInjector::inject(@self)
- David Grudl
- Nette Core | 8227
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
@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)
- mishak
- Člen | 94
@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
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)
- bene
- Člen | 82
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
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
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
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
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;
}