Je návrhově v pořádku vytahovat závislosti pro komponentu přímo z DI kontejneru?
- janturon
- Člen | 5
Máme v projektu komponenty, které mají více závislostí (až 8), předávání přes konstruktor vyžaduje rostoucí množství parametrů. Pokud komponenta nepotřebuje nic, co by nebylo v DI kontejneru, je návrhově v pořádku předat jí DI kontejner a získat závislosti odtamtud?
use Nette\DI\Container;
class MyControl extends BaseControl
{
private Depencency1 $obj1;
private Depencency2 $obj2;
... další závislosti
function __construct(Container $di)
{
$this->obj1 = $di->getByType(Dependency1::class);
$this->obj2 = $di->getByType(Dependency2::class);
... další závislosti
}
}
Pokud ne, existuje nějaký jiný implementovaný vzor (např. Mediátor), který má Nette k dispozici? Vím o automaticky generované továrně:
interface IMyControlFactory
{
public function create(): MyControl;
}
Ta však vyžaduje vložení závislosti na všech místech volání
use App\Controls\Custom\IMyControlFactory;
...
/** @var IMyControlFactory @inject */
public IMyControlFactory $myControlFactory;
...
protected function createComponentMy(): MyControl
{
return $this->myControlFactory->create();
}
Při předání DI kontejneru mohu použít kratší zápis bez továrny
protected function createComponentMy(): MyControl
{
return new MyControl($this->di); // Container má Presenter k dispozici
}
Je to tak v pořádku?
Editoval janturon (20. 1. 2021 16:34)
- dakur
- Člen | 493
A v čem je problém s 8 závislostmi? V PHP 8 to máš ještě lepší:
public function __construct(
private Dependency1 $obj1,
private Dependency2 $obj2,
private Dependency3 $obj3,
private Dependency4 $obj4,
private Dependency5 $obj5,
private Dependency6 $obj6,
private Dependency7 $obj7,
private Dependency8 $obj8,
)
{}
vs.
private Dependency1 $obj1,
private Dependency2 $obj2;
private Dependency3 $obj3;
private Dependency4 $obj4;
private Dependency5 $obj5;
private Dependency6 $obj6;
private Dependency7 $obj7;
private Dependency8 $obj8;
public function __construct(Container $di)
{
$this->obj1 = $di->getByType(Dependency1::class);
$this->obj2 = $di->getByType(Dependency2::class);
$this->obj3 = $di->getByType(Dependency3::class);
$this->obj4 = $di->getByType(Dependency4::class);
$this->obj5 = $di->getByType(Dependency5::class);
$this->obj6 = $di->getByType(Dependency6::class);
$this->obj7 = $di->getByType(Dependency7::class);
$this->obj8 = $di->getByType(Dependency8::class);
}
Editoval dakur (20. 1. 2021 16:21)
- janturon
- Člen | 5
Problém je v rostoucím množství parametrů v konstruktoru, tedy v jeho volání, kde je nutno ty parametry předat. Řešením je rozhraní pro továrnu (doplnil jsem do otázky po tvé odpovědi), ale náš projekt má pak celý adresář souborů typu:
<?php
declare(strict_types=1);
namespace App\Controls\MyControl;
interface IMyControlFactory
{
public function create(): Core\Controls\MyControl;
}
V podstatě potřebujeme funkčnost „vytvoř automaticky komponentu, která vše, co potřebuje, má v DI Containeru“ bez zbytečného kódu. Napadlo mne pouze řešení s předáním Containeru v parametru, ale kolegovi se to nepozdává. Existuje nějaký racionální argument, proč tak nepostupovat? Pokud ano, existuje jiná možnost, např. zařídit automatickou tvorbu těch továren, které jsou všechny stejné až na typ?
Editoval janturon (20. 1. 2021 23:14)
- dakur
- Člen | 493
protected function createComponentMy(): MyControl
{
return new MyControl($this->di); // Container má Presenter k dispozici
}
Problém tkví v tomto kódu, protože produkuje skrytou závislost na
MyControl
, který nejde přes constructor, ale je z ničeho nic
použitý až kdesi dole. Když si ovšem předáš MyControl
přes
constructor, tak se při kompilaci DI začne vykonávat její konstruktor, což
typicky úplně nechceš, protože se pak zbytečně vykonávají constructory
všech komponent, které se třeba v daném view vůbec nepoužívají. Proto
je tam factory, která toto odstiňuje (tzv. lazyloading component) – vykoná
se jen constructor factory a až teprve při zavolání create()
se
vykoná constructor komponenty.
V Nette stačí na factory jen interface, což má hned dvě výhody:
- DI ti z toho interface vygeneruje factory class samo
- a to včetně všech závislostí, takže pak právě nemusíš řešit jejich předávání
Factory má tedy svůj význam a pozitiva (developer experience, performance) převažují nad nevýhodami (code style, file noise).
- dakur
- Člen | 493
Jo a co se týče automatické tvorby továren, můžeš si na to
samozřejmě napsat jednoduchý tool využívající např.
nette/php-generator
, ale upřímně mi to nepřijde úplně
přínosné – než ten tool spustíš a nacvakaš do něj název interface,
tak ho stejně rychle napíšeš v IDE. 🙂
Editoval dakur (21. 1. 2021 7:42)
- jiri.pudil
- Nette Blogger | 1032
Napadlo mne pouze řešení s předáním Containeru v parametru, ale kolegovi se to nepozdává. Existuje nějaký racionální argument, proč tak nepostupovat?
Je to případ typu: „Jean, přineste mi klavír, odložil jsem si na něj doutník.“
Degraduješ tím DI kontejner na service locator, což se dnes považuje za anti-pattern. Popsaly se o tom tisíce bajtů textů, ocituji tady kupříkladu tenhle od @DavidGrudl :
Bohužel Service Locator není v souladu s DI.
Proč? Není v souladu s tím, že předávání závislostí je zřejmé a že se třída ke svým závislostem otevřeně hlásí. Třída
Authenticator
- potřebuje databázi, ale hlásí se k velmi obecnému service locatoru, což je v naprostém nepoměru vůči tomu, co skutečně potřebuje
- že potřebuje zrovna databázi se nedá zjistit jinak, než zkoumáním její implementace
Třída se tedy musí hlásit ke všem svým závislostem a právě jen k nim. Jinak o svých závislostech lže.
- jiri.pudil
- Nette Blogger | 1032
Ještě s dovolením přidám nevyžádaný názor, protože mám dojem, že od začátku řešíš důsledek a přehlížíš příčinu :)
Máme v projektu komponenty, které mají více závislostí (až 8), předávání přes konstruktor vyžaduje rostoucí množství parametrů
Připadá mi kontraproduktivní hledat způsob, jak si usnadnit jejich předání. Pokud má třída hodně závislostí, a já dokonce přidávám nějakou další, mělo by mě to praštit do očí a přimět k zamyšlení:
- jestli toho ta třída náhodou nedělá příliš a nestálo by za to ji rozdělit na menší jednotky;
- jestli mám správně navržené chování a interakce závislostí a nestálo by za to např. sáhnout po návrhových vzorech jako facade.