Slovník pojmů
Dependency Injection: co to je a proč ji používat
Závislosti třídy jsou viditelné v jejím rozhraní a lze je při testu nebo změně infrastruktury nahradit.
Stručná definice
Třída si nemusí sama vytvářet vše, s čím pracuje.
Závislostí může být repozitář, klient externího API, logger, fronta zpráv nebo odesílač e-mailu. Pokud si třída takový objekt vytvoří uvnitř, pevně se váže na jednu implementaci a hůře se testuje. Při dependency injection dostane potřebný objekt při svém vytvoření.
Jde o návrhový princip, ne o vlastnost pouze Symfony. Framework může dodat DI container, který vytvoření a propojení služeb automatizuje, ale stejného principu lze dosáhnout i bez něj prostým předáním objektu do konstruktoru.
Použití
Kde předávání závislostí pomáhá
Největší přínos má na hranicích mezi aplikační logikou a technickým okolím.
- import objednávek s klientem marketplace a repozitářem
- služba pro zásilky s různými implementacemi dopravců
- use case volaný z HTTP controlleru, CLI příkazu i handleru zprávy
- test bez skutečného API, databáze nebo fronty
- výměna infrastruktury na jednom místě konfigurace
Praktický příklad
Import marketplace objednávky
Třída pro import potřebuje načíst externí objednávku a uložit její lokální podobu. Neví ani nemusí vědět, zda klient mluví RESTem a zda repository používá Doctrine. Tyto volby patří do konfigurace aplikace.
interface MarketplaceOrderClient
{
public function fetch(string $externalId): ExternalOrder;
}
final class ImportMarketplaceOrder
{
public function __construct(
private MarketplaceOrderClient $client,
private OrderRepository $orders,
) {
}
public function import(string $externalId): void
{
$this->orders->save(Order::fromExternal($this->client->fetch($externalId)));
}
}
Jak funguje
Od deklarace potřeby k sestavení aplikace
Povinné závislosti je nejčitelnější deklarovat přímo v konstruktoru.
- Deklarace Třída ve svém konstruktoru uvede, co opravdu potřebuje.
- Složení Konfigurace nebo composition root určí konkrétní implementace.
- Použití Třída pracuje přes veřejný kontrakt předaného objektu.
- Test Test může předat řízený fake, stub nebo mock namísto vzdálené služby.
- Změna Technickou implementaci lze změnit bez zásahu do obchodního rozhodování.
Důležité vlastnosti
Constructor injection, container a rozhraní
DI pomáhá jen tehdy, když zpřesňuje skutečné hranice a odpovědnosti.
Constructor injection
Je vhodná pro závislosti, bez nichž objekt nemůže plnit svou úlohu. Objekt nevznikne v částečně nakonfigurovaném stavu. Setter injection se hodí spíše pro skutečně volitelné chování.
DI container a autowiring
Container eviduje, jak služby vytvořit a propojit. Autowiring vyřeší jednoznačné typy; více implementací, hodnoty a výjimky potřebují explicitní konfiguraci.
Rozhraní
Rozhraní je užitečné pro skutečný kontrakt nebo reálně zaměnitelnou implementaci. Vytvářet je pro každou třídu jen kvůli DI přidává šum bez hodnoty.
Service locator
U DI třída závislosti otevřeně deklaruje. Obecný locator či celý container uvnitř třídy je naopak skrývá; úzce vymezený locator je výjimka, ne běžná náhrada.
Malý příklad
Constructor injection v PHP
Use case zná kontrakty, ne konkrétní technické implementace.
- MarketplaceOrderClient
- Načte externí objednávku; produkce může použít REST klienta, test připravený fake.
- OrderRepository
- Uloží objednávku; implementace může používat Doctrine nebo jiné úložiště.
- Composition root
- Místo pro konkrétní vstupní bod, kde se rozhodne, která implementace se použije.
- Aplikační služba
- Řeší rozhodnutí importu bez přímého vytváření HTTP klienta či připojení k databázi.
Výhody a omezení
DI není automatická dobrá architektura
Přínosy
- potřeby třídy jsou čitelné přímo z konstruktoru
- test nemusí otevírat skutečnou síť ani databázi
- infrastrukturu lze měnit na jednom místě
- pomáhá rozlišit obchodní pravidlo od technického detailu
Časté chyby
- třída s mnoha závislostmi místo rozdělení odpovědností
- rozhraní pro každou interní třídu bez skutečného kontraktu
- natažení celého containeru do domény, entity nebo controlleru
- představa, že DI samo vyřeší špatná pravidla, data nebo transakce
Hranice použití
Abstrakce má mít konkrétní důvod.
Pokud přibude druhý marketplace, není nutné měnit jádro importu, jen když oba zdroje skutečně sdílejí smysluplný kontrakt. DI není důvodem předstírat jednotné rozhraní tam, kde se obchodní chování podstatně liší.
Třída s dvanácti povinnými službami bývá spíš signálem, že spojuje příliš mnoho odpovědností. Správnou reakcí není další konfigurace containeru, ale zjednodušení případu použití nebo rozdělení služby.
Na co myslet
Jasné závislosti místo skrytého globálního stavu
Princip se vyplácí ověřovat v testech i při code review.
- povinné závislosti přes constructor injection
- výslovná konfigurace pro nejednoznačné implementace
- rozhraní jen pro reálný kontrakt nebo změnu chování
- nepředávat celý container do běžné aplikační třídy
- testy pro podstatné hranice mezi logikou a infrastrukturou
Časté otázky
Co je dependency injection
Je dependency injection totéž co DI container?
Ne. Dependency injection je princip předávání závislostí. DI container je nástroj, který jejich vytváření a propojování může automatizovat.
Musí mít každá služba vlastní rozhraní?
Ne. Rozhraní je vhodné pro významný kontrakt nebo reálně zaměnitelnou implementaci. Pro jednu interní třídu bez takové potřeby jen přidává další vrstvu kódu.
Proč je constructor injection obvykle lepší než setter injection?
Povinné závislosti jsou viditelné a objekt nemůže vzniknout bez nich. Setter se hodí spíše pro skutečně nepovinné chování.
Je service locator vždy chyba?
Ne vždy, úzce omezený locator může být vhodný pro lazy výběr předem deklarovaných služeb. Celý container v běžné aplikační třídě ale obvykle skrývá závislosti a komplikuje testování.
Jak dependency injection používám v praxi
Rozsah vrstev volím podle složitosti problému.
U složitějších částí odděluji aplikační logiku od databáze, HTTP a externích služeb pomocí jasných závislostí a rozhraní.