Dependency Injection Factories: konečně použitelné továrničky

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Filip Procházka
Moderator | 4668
+
0
-

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:

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
+
0
-

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
+
0
-

@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)

vvoody
Člen | 910
+
0
-

Metodu injectNeco je mozne pouzit aj v

HosipLan napsal(a):

…komponentu v komponentě v komponentě …

?

Editoval vvoody (23. 7. 2012 11:43)

blindAlley
Člen | 31
+
0
-

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
+
0
-

@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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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.

Editoval HosipLan (23. 7. 2012 12:59)

blindAlley
Člen | 31
+
0
-

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.

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
+
0
-

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 :)

jasir
Člen | 746
+
0
-

Mě se to líbí moc, o něčem podobném jsem přemýšlel a cítil jsem, že by mi to dost usnadnilo práci.
Jsem pro to to dát zatím jako rozšíření a uvidíme, jak to funguje v praxi.
Jaké jsou přesně ty problémy s IDE?

Interface se generuje také (při neexistenci) ?

Filip Procházka
Moderator | 4668
+
0
-

Interface se generuje vždy. Jde o to, abych toho musel napsat co nejméně.

Elijen
Člen | 171
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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)

Filip Procházka
Moderator | 4668
+
0
-

Tak nám to Davídek doladil a mergl! Slavíme!

pekelnik
Člen | 462
+
0
-

Skvela prace :)

bazo
Člen | 620
+
0
-

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
+
0
-

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
+
0
-

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

Filip Procházka
Moderator | 4668
+
0
-

Samozřejmě je to chyba ;) Už je to správně.

bazo
Člen | 620
+
0
-

ok, parada. cize je to uple rovnake ako bez multiplieru :)

arguments netreba uvadzat, doplnia sa autowirom

Editoval bazo (26. 11. 2012 15:33)

bazo
Člen | 620
+
0
-

ale predsalen je tu problem,

Component with name ‚jednotka‘ does not exist

sablona {control nowPlaying-jednotka}

cize predsalen treba niekde predavat aspon ten $name

edit: tak netreba, zabudol som pridat return do tovarnicky multipliera

Editoval bazo (26. 11. 2012 15:48)

bazo
Člen | 620
+
0
-

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
+
0
-

Ano, jde

interface IFooFactory
{
    /** @return Foo */
    function create($something);
}
factories:
	interface: IFooFactory
	parameters: [something]
	setup:
		- setSomething(%something%)

Editoval HosipLan (3. 12. 2012 11:34)

bazo
Člen | 620
+
0
-

aha, uz mi to dochadza. diky

bazo
Člen | 620
+
0
-

no konecne som to skusil, ale zakrici to na mna

Nette\DI\ServiceCreationException

Service ‚object‘: Missing item ‚something‘.

Filip Procházka
Moderator | 4668
+
0
-

Sory… zkus teď ;)

bazo
Člen | 620
+
0
-

to preda len string. no skusil som uz asi vsetky kombinacie, nakoniec uz ani nepotrebujem, co som potreboval. ale skombinovat autowire a predavanu hodnotu cez create sa asi neda.

namiesto parameters by malo byt arguments, vtedy mozes tu premennu podsunut rucne

ZZromanZZ
Člen | 87
+
0
-

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
+
0
-

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
+
0
-

Ještě bych doplnil, že v inject metodě musí být typehint IMenuFactory.

ZZromanZZ
Člen | 87
+
0
-

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
+
0
-

Musí to fungovat i na interface. Jenom tam musíš napsat celou cestu i s namespacem.

ZZromanZZ
Člen | 87
+
0
-

Lehce jsem nahlídl do kódu, a je tam funkce class_exists(), na které to vlastně stojí a padá.
A která nekontroluje existenci interface.