Dependency Injection Factories: konečně použitelné továrničky
- Filip Procházka
- Moderator | 4668
Nové řešení
Byl zvolen nový přístup, témeř dokonalý :) pro detaily přeskočte až sem
Staré řešení
Továrničky se tváří na první pohled strašně super. Když se přeneseme přes to vytváření „entity“ v DIC v dokumentaci a použijeme je na něco rozumného tak jsou ještě víc super.
Má to jeden háček. Všude kde chcete použít takovouto továrničku na
komponentu, musíte mít přístupný Container
. V presenteru to
tak nebolí, tam je vždy, ale co když chci vytvářet komponentu
v komponentě v komponentě v… (no snad se chápeme :) Předávat si
Container
do komponenty nepřipadá v úvahu, přece nebudeme
přenášet celé piáno, když chceme jen doutník.
factories:
paginator:
class: VisualPaginator
Co se tedy dá dělat, pokud chci tuhle feature nějak rozumně používat (a pokud možno využít ještě i autowire)?
Vytvoříme si nějaký interface pro továrnu
interface IPaginatorFactory
{
/** @return VisualPaginator */
function create();
}
a tento interface pak budeme vyžadovat
class ViewPresenter extends BasePresenter
{
private $paginatorFactory;
public function injectPaginatorFactory(IPaginatorFactory $paginatorFactory)
{
$this->paginatorFactory = $paginatorFactory;
}
protected function createComponentVp()
{
return $this->paginatorFactory->create();
}
}
Celé tohle děláme, abychom mohli použít autowire, nebo alespoň ručně konfigurovat služby do komponent. Takže podle toho továrničku implementujeme
class PaginatorFactory implements IPaginatorFactory
{
private $container;
public function __construct(Nette\DI\Container $container)
{
$this->container = $container;
}
public function create()
{
// tuto metodu generuje DIC
return $this->container->createPaginator();
}
}
A teď zaregistrujeme jako službu a autowire se postará o zbytek.
services:
paginatorFactory:
class: IPaginatorFactory
factory: PaginatorFactory
Takže postup by byl. Teď by bylo dobré ho zautomatizovat. Asi se ode mě čeká ukázka funkční implementace že :)
Schválně jsem to udělal tak, že se vždy generuje interface ve tvaru
%namespace%\I%nazevTridy%Factory
.
- Co si o tom myslíte?
- Mám to dát na packagist jako rozšíření?
- Napadá vás, jak to udělat lépe?
- Absolutně si nejsem jistý tím názvem interface (hlavně namespacem). Ale když je ve stejném, tak se mi to subjektivně lépe používá. Jak by jste ten interface pojmenovali vy?
první problémy:
- generuju třídu s „cizím“ namespacem
- IDE z toho není moc nadšené
první dojmy:
- až na podporu v IDE je to fantastické :)
Alternativní řešení (inspirováno diskuzemi na Nette jabberu):
Interface psát ručně a předávat ho v konfiguraci továrničce
pro:
- IDE si s tím lépe poradí
- je to více „statické“ – intefacy nejsou generovány a jsou vždy k dispozici
proti:
- nechce se mi to psát ručně, když je možnost to nepsat ručně
Použít generické služby
pro:
- udělá se jeden interface a pak se budou vyžadovat jeho ruzné implementace
/**
* @param Nette\DI\IServiceFactory <VisualPaginator> $factory
*/
function __construct(Nette\DI\IServiceFactory $factory) { }
- nejpřirozenější syntaxe
- nejmíň psaní
proti:
- podpora v IDE (největší magie), našeptávání
Editoval HosipLan (30. 10. 2012 1:07)
- blindAlley
- Člen | 31
Viděl bych to jako maximálně jako nepovinné rozšíření, ne jako výchozí způsob vytváření komponent ve frameworku. Klasické factory metody v presenterech s voláním new jsou podle mě většinou dostačující a potřeba použití DIC je dost omezená.
Dále lze kontainer získat v komponentě přes metodu
$this->getPresenter()->getContext();
Sice to funguje jen v případech, kdy už je komponenta k němu připojena, ale to bude téměř vždy, zejména pokud jsou komponenty vytvářeny jako lazy.
- Filip Procházka
- Moderator | 4668
@blindAlley:
lasické factory metody v presenterech s voláním new jsou podle mě většinou dostačující a potřeba použití DIC je dost omezená.
Maximálně nesouhlasím.
Dále lze kontainer získat v komponentě přes metodu
Porušuješ DI.
Nastuduj si prosím souvislosti, než budeš mít další připomínky. Díky.
Editoval HosipLan (23. 7. 2012 11:22)
- blindAlley
- Člen | 31
Maximálně nesouhlasím.
To je názor proti názoru, naše zkušenost s Nette aplikacemi je taková, že jen mizivé procento komponent by mělo význam vytvářet přes DIC. Protože je to je výkonově náročnější a klasický způsob lze na DIC jednoduše v případě potřeby převést není důvod k tomu to dělat vždy.
Jsem prostě proto, ať si každý vybere, jak chce komponenty vytvářet a framework mu nenutí DI.
Porušuješ DI.
Presenter je v případě vytváření komponent vlastně DI kontainerem a pokud na něm již komponenta závisí tak není nic špatného ho použít pro její vytvoření.
- Filip Procházka
- Moderator | 4668
@vvoody: A teď mi řekni, jak injektneš
nesdílenou službu? Nijak, protože chceš injektovat její továrnu. A když
dáš do typehintu Nette\Callback
(protože takto jsou
v současné době implementované továrničky), tak autowire nebude vědět,
co ti tam má předat.
Už si rozumíme, jaký problém se snažím vyřešit?
Editoval HosipLan (23. 7. 2012 11:54)
- Filip Procházka
- Moderator | 4668
blindAlley napsal(a):
To je názor proti názoru, naše zkušenost s Nette aplikacemi je taková, že jen mizivé procento komponent by mělo význam vytvářet přes DIC. Protože je to je výkonově náročnější a klasický způsob lze na DIC jednoduše v případě potřeby převést není důvod k tomu to dělat vždy.
Já zase uznávám znovupoužitelnost a vzhledem k tomu, že tě nikdo nenutí používat továrničky na vytváření komponent, nechápu proč proti tomu protestuješ.
Dále pak nikde netvrdím, že je potřeba vždy vytvářet komponenty pomocí DI, to jsi vyčetl kde prosím?
Jsem prostě proto, ať si každý vybere, jak chce komponenty vytvářet a framework mu nenutí DI.
Jéje! A kde ti nutím DI? Nikde. Já ho ale chci používat. A striktně.
Presenter je v případě vytváření komponent vlastně DI kontainerem a pokud na něm již komponenta závisí tak není nic špatného ho použít pro její vytvoření.
Komponenta nemá co záviset na DI Containeru, Presenter nemá co záviset na DI Containeru. To není věc názoru ale fakt. Pokud chceš používat service locator, nikdo ti nebrání. Ale já chci používat DI. Striktně.
- blindAlley
- Člen | 31
Já to pochopil tak, že to chceš aby framework při požadavku na komponentu zavolal DIC a ten by veděl, jakou factory třídu zavolat, takže by se už nepsaly factory metody v presenterech ale jen injectXXX. Pokud to tak není a dál zůstává to, že framework jen zavolá factory metodu na presenteru při požadavku na novou komponentu pak v pořádku a špatně jsem to pochopil.
Co tedy přesně představuje Tvůj návrh, jen ta compiler extension?
… Presenter nemá co záviset na DI Containeru.
Ale závisí, až skutečně zmizí __construct( string $baseDir, Nette\DI\Container $context ) z Presenteru, tak teprve pak je možné se o nějakém porušení DI při volání getContext() bavit.
- blindAlley
- Člen | 31
Ale závisí, až skutečně zmizí __construct( string $baseDir, Nette\DI\Container $context )
V presenteru je koukám __construct(Nette\DI\Container $context), což ale na věci nic nemění.
- Filip Procházka
- Moderator | 4668
blindAlley napsal(a):
Já to pochopil tak, že to chceš aby framework při požadavku na komponentu zavolal DIC a ten by veděl, jakou factory třídu zavolat, takže by se už nepsaly factory metody v presenterech ale jen injectXXX. Pokud to tak není a dál zůstává to, že framework jen zavolá factory metodu na presenteru při požadavku na novou komponentu pak v pořádku a špatně jsem to pochopil.
Ano, špatně jsi to pochopil. Chci aby libovolná třída registrovaná/vytvářená přes DIC (tedy i presenter) mohla vyžadovat továrničku na (třeba) komponentu
public function __construct(IPaginatorFactory $factory)
{
$this->paginatorFactory = $factory;
}
A potom tuto továrničku na službu budu volat v továrničce na komponentu
protected function createComponentPaginator()
{
return $this->paginatorFactory->create();
}
Tohle je navíc, oproti současnému chování, kdy komponentu vytváříš „ručně“ v továrničce na komponentu. Díky tomu mohu mít komponentu jako takovou v DIC a injektovat do ní další služby (a další továrničky na komponenty, …).
… Presenter nemá co záviset na DI Containeru.
Ale závisí, až skutečně zmizí __construct( string $baseDir, Nette\DI\Container $context ) z Presenteru, tak teprve pak je možné se o nějakém porušení DI při volání getContext() bavit.
Evidentně nesleduješ vývoj, takže ti to přiblížím.
- Presenter už není přímo závislý na DIC
- Na předávání závislostí je možné použít metodu inject
- DIC se do presenteru předává pouze kvůli tomu, aby byla dodržena zpětná kompatibilita.
Editoval HosipLan (23. 7. 2012 12:59)
- blindAlley
- Člen | 31
Tohle je navíc, oproti současnému chování …
To je pak tedy v pořádku, asi se Ti tedy jedná opravdu jen o to dostat tu extension do DIC do frameworku? Proti tomu nic nemám, bude to užitečné, když to budeme u nějakých komponent DIC chtít použít, občas se to hodí. Zatím to může zůstat klidně jako rozšíření mimo framework ne?
Evidentně nesleduješ vývoj, takže ti to přiblížím.
- Presenter už není přímo závislý na DIC
Díky, tohle jsem skutečně nevěděl, takže pak je opravdu $this->getPresenter()->getContext() porušením DI.
- Filip Procházka
- Moderator | 4668
blindAlley napsal(a):
To je pak tedy v pořádku, asi se Ti tedy jedná opravdu jen o to dostat tu extension do DIC do frameworku?
Přesně tak.
Zatím to může zůstat klidně jako rozšíření mimo framework ne?
Zatím to není ani zdaleka tak super, jak by to mohlo být, kdyby se vymyslelo, jak to udělat pěkně a kdyby to podporovalo IDE. Takže se nemusíš bát, že bych já tlačil do FW něco co považuju za nedokonalé, nebo David něco takového přijal :)
- Filip Procházka
- Moderator | 4668
Interface se generuje vždy. Jde o to, abych toho musel napsat co nejméně.
- Elijen
- Člen | 171
V současné době toto dělám ručně a rozhodně bych ocenil automatizaci. Až bude trochu více času vyzkouším tvé řešení – vypadá to moc zajímavě. Zatím naslepo:
- Pokud interface již někde existuje nehrozí zhroucení aplikace? (duplicitní definice třídy)
- Jako řešení duplicity bych použil uživatelsky definovaný namespace (podobně jako jde u doctrine definovat namespace pro entity).
- Také ze screenu přesně nedokáži určit jaké problémy s tím může IDE mít (z anotací přeci pozná, jaké třídy se vrací instance).
- Filip Procházka
- Moderator | 4668
Vytvořil jsem nový pullrequest, který na základě předaného interface generuje injektovatelné továrničky: https://github.com/…tte/pull/839 (to samé co předtím, ale nyní je třeba napsat interface)
- norbe
- Backer | 405
Moc nechápu jakou výhodu má to, že si interface musím napsat ručně. Můžeš mi to vysvětlit? Předchozí verzi jsem nezkoušel, ale stejně jako Elijen nechápu, proč by nemělo fungovat napovídání z vygenerovaného interfacu (SystemContainer se taky generuje a minimálně netbeans napovídá).
- Filip Procházka
- Moderator | 4668
To že se ti generuje třída je pochopitelné, můžeš záviset na jejím interface a implementace je ti ukradená. To že se generuje interface, na kterým závisíš a který ti bude mimo jiné chybět v API dokumentaci, už ti tak ukradené není.
Další nemilý problém je tohle. Dokud aplikaci nespustíš, tak interfacy neexistují. A pokud aplikaci nemáš jak spustit a máš „pouze“ testy (protože třeba píšeš knihovnu) tak jsi úplně ztracen.
Zkoušel jsem oba přístupy a tohle zkrátka rock-solid a funguje to perfektně
class Article
{
public $title;
public function __construct($title) { $this->title = $title; }
}
interface IArticleFactory
{
/** @return Article */
function create($title);
}
A jednoduchá konfigurace
factories:
article:
class: Article
factory: IArticleFactory
arguments: [%title%]
parameters: [title]
A dokonce díky tomu, že napíšeš interface, tak se ti konfigurace ještě zjednodužší, protože spousta věcí se z něj dá odvodit ;)
factories:
article: IArticleFactory
Editoval HosipLan (3. 11. 2012 19:13)
- bazo
- Člen | 620
mna by zaujimalo ako sa to da skombinovat s multiplierom
priklad, terajsi stav, tovarnicka pouzita v niekolkych presenteroch
protected function createComponentNowPlaying() {
return new Multiplier(function($channel, Multiplier $multiplier) use ($repository1, $repository2) {
$nowPlaying = new Components\NowPlaying($multiplier, $channel, $repository1, $repository2);
$nowPlaying->setChannel($channel);
});
}
ako k tomuto napisem interface a config? tu sa nedaju odignorovat tie prve dva parametre predavane komponente nowPlaying.
diky
Editoval bazo (26. 11. 2012 11:38)
- Filip Procházka
- Moderator | 4668
komponenta
namespace Components;
class NowPlaying extends Nette\Application\UI\Control
{
public function __construct($repository1, $repository2)
{
parent::__construct();
// ..
}
}
Multiplier tam predavat nemusis a $channel
taky ne. V momentě
připojení je budeš mít přístupné jako $this->parent
a
$this->name
.
interface
namespace Components;
interface INowPlaying
{
/** @return NowPlaying */
function create();
}
konfigurace
factories:
nowPlaying:
class: Components\NowPlaying
implement: Components\INowPlaying
arguments: [@repositoryOne, @repositoryTwo]
použití
private $nowPlayingFactory;
public function injectNowPlaying(Components\INowPlaying $factory)
{
$this->nowPlayingFactory = $factory;
}
protected function createComponentNowPlaying()
{
$factory = $this->nowPlayingFactory;
return new Multiplier(function($channel) use ($factory) {
return $factory->create();
});
}
Editoval HosipLan (26. 11. 2012 14:43)
- bazo
- Člen | 620
hm, vzdy som si myslel, ze komponenty vyrabane cez multiplier musia dostat v konstruktore parent aj name.
ale je tu jeden problem, neviem ci tam chyba dvojbodka za interface
ak ju doplnim vyhodi mi to tuto chybu
Nette\DI\ServiceCreationException
Service ‚nowPlaying‘: Unknown or deprecated key ‚interface‘ in definition of service.
nette by malo byt najnovsie
- bazo
- Člen | 620
ide spravit nieco taketo?
interface tovarnicky
interface IFactory {
/** @return Object */
public function create($something);
}
vygenerovana tovarnicka
final class IFactoryImpl_object implements IFactory
{
private $container;
public function __construct(Nette\DI\Container $container)
{
$this->container = $container;
}
public function create($something)
{
$service = new Object($this->context->getSomeServiceNeededInConstructor());
$this->container->callInjects($service);
$service->setSomething($something);
return $service;
}
}
config.neon ?
- Filip Procházka
- Moderator | 4668
Ano, jde
interface IFooFactory
{
/** @return Foo */
function create($something);
}
factories:
interface: IFooFactory
parameters: [something]
setup:
- setSomething(%something%)
Editoval HosipLan (3. 12. 2012 11:34)
- ZZromanZZ
- Člen | 87
Rád bych dokonale pochopil novou metodu předávání služeb do komponent v Nette 2.1+
Je následující kód rámcově správně ?
class HomepagePresenter extends BasePresenter {
/** @var IMenuFactory */
private $menuFactory;
public function injectMenuFactory(IMenuFactory $factory) {
$this->menuFactory = $factory;
}
protected function createComponentMenu() {
return $this->menuFactory->create();
}
}
interface IMenuFactory {/** @return Menu */ function create(); }
class Menu extends \Nette\Application\UI\Control {
/** @var SomeService */
private $someService;
public function __construct(SomeService $service) {
parent::__construct();
$this->someService = $service;
}
public function render() {
$this->template->setFile(__DIR__ . DIRECTORY_SEPARATOR . "menu.latte");
$this->template->something = $this->someService->getSomething();
$this->template->render();
}
config.neon:
services:
someService:
class: SomeService
factories:
MenuFactory:
implement: IMenuFactory
// edit: Upraveno na základě podnětů od Filip Procházka a jiri.pudil. Díky!
Editoval ZZromanZZ (14. 3. 2013 21:23)
- Filip Procházka
- Moderator | 4668
třída Menu
nemá implementovat
IMenuFactory
, protože její implementaci ti
vygeneruje Nette a zároveň zařídí autowire.
Stačí takto
services:
someService:
class: SomeService
factories:
MenuFactory:
implement: IMenuFactory
- jiri.pudil
- Nette Blogger | 1029
Ještě bych doplnil, že v inject metodě musí být typehint
IMenuFactory
.
- ZZromanZZ
- Člen | 87
Lze využít automatické injectování pomocí @inject anotace (aktuálně implemetováno v dev verzi) i na továrničky ?
U služby to není problém. Ale u továrny MenuFactory (viz můj kód výše) pomocí následujího to nelze.
/**
* @var IMenuFactory
* @inject
*/
public $menuFactory;
Funguje to totiž jenom na klasické třídy ne na interface. Jde to obejít nebo existuje jiný přístup?
- Filip Procházka
- Moderator | 4668
Musí to fungovat i na interface. Jenom tam musíš napsat celou cestu i s namespacem.