Je návrhově v pořádku vytahovat závislosti pro komponentu přímo z DI kontejneru?

janturon
Člen | 3
+
0
-

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. 16:34)

dakur
Člen | 188
+
0
-

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. 16:21)

janturon
Člen | 3
+
0
-

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. 23:14)

dakur
Člen | 188
+
+4
-
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:

  1. DI ti z toho interface vygeneruje factory class samo
  2. 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 | 188
+
+1
-

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

janturon
Člen | 3
+
0
-

Skvělé, díky.

jiri.pudil
Nette Blogger | 961
+
+6
-

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 | 961
+
+8
-

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.