Często zadawane pytania o DI (FAQ)
- Czy DI to inna nazwa IoC?
- Czym jest Service Locator?
- Kiedy lepiej nie używać DI?
- Czy używanie DI ma wady?
- Jak przepisać starą aplikację na DI?
- Dlaczego kompozycja jest preferowana nad dziedziczeniem?
- Czy Nette DI Container da się używać poza Nette?
- Dlaczego konfiguracja jest w plikach NEON?
- Czy parsowanie plików NEON spowalnia aplikację?
- Jak sięgnąć w klasie po parametry z pliku konfiguracyjnego?
- Czy Nette obsługuje interfejs Container z PSR-11?
- Co oznaczają terminy container, compiler, definition itd.?
Czy DI to inna nazwa IoC?
Inversion of Control (IoC) to zasada opisująca przepływ sterowania w programie: czy Twój kod wywołuje kod
zewnętrzny, czy też kod zewnętrzny (jak framework) wywołuje Twój kod? IoC to szerokie pojęcie obejmujące zdarzenia, tak zwaną zasadę hollywoodzką i inne aspekty. Pojęcie
to obejmuje również fabryki, omówione w zasadzie nr 3:
niech zajmie się tym fabryka, które stanowią odwrócenie operatora new.
Dependency Injection (DI) skupia się na tym, jak obiekty pozyskują swoje zależności (czyli inne obiekty, z którymi muszą pracować). To wzorzec projektowy zalecający jawne przekazywanie zależności obiektom, zamiast pozwalać im tworzyć je albo odnajdywać.
DI można więc uznać za konkretną formę IoC. Nie wszystkie formy IoC sprzyjają jednak czystemu kodowi. Antywzorcami są na przykład techniki opierające się na stanie globalnym albo na wzorcu Service Locator.
Czym jest Service Locator?
To alternatywne podejście do wstrzykiwania zależności. Polega na istnieniu centralnego obiektu (lokatora), w którym rejestrowane są wszystkie dostępne usługi (zależności). Gdy obiekt potrzebuje zależności, prosi o nią Service Locator.
W porównaniu z DI brakuje mu jednak przejrzystości. Zależności są ukryte w kodzie obiektu (wywołania lokatora), zamiast być jawne w jego API (konstruktorze albo metodach), więc zrozumienie powiązań wymaga zajrzenia do kodu. Trudniejsze jest też testowanie, bo nie da się po prostu przekazać atrap zależności przy tworzeniu instancji obiektu; często trzeba manipulować samym Service Locatorem. Ponadto Service Locator wprowadza zbędną zależność: obiekty stają się powiązane z lokatorem, inaczej niż przy DI, gdzie obiekty najlepiej w ogóle nie wiedzą o kontenerze.
Kiedy lepiej nie używać DI?
Nie są znane istotne wady poprawnego stosowania wzorca projektowego wstrzykiwania zależności. Wręcz przeciwnie, pozyskiwanie zależności z globalnie dostępnych miejsc (jak właściwości statyczne czy singletony) prowadzi do licznych komplikacji, podobnie jak używanie Service Locatora. Dlatego używanie DI jest zwykle zawsze wskazane. To nie dogmat; po prostu żadna lepsza alternatywa czystego zarządzania zależnościami nie przyjęła się szeroko.
Istnieją jednak konkretne, ograniczone sytuacje, w których globalny dostęp do obiektów bywa dopuszczalny. Na przykład podczas debugowania, gdy potrzebujesz zrzucić wartość zmiennej, zmierzyć czas wykonania albo zalogować komunikat w konkretnym miejscu. W tych przypadkach, dotyczących działań tymczasowych, które później zostaną z kodu usunięte, użycie globalnie dostępnego dumpera, timera albo loggera może być uzasadnione. Narzędzia te nie są częścią rdzennego projektu aplikacji.
Czy używanie DI ma wady?
Czy używanie wstrzykiwania zależności wprowadza wady, takie jak więcej pisania kodu albo niższa wydajność? Co tracimy, gdy zaczynamy pisać kod zgodnie z DI?
Samo DI ma pomijalny wpływ na wydajność w czasie działania i zużycie pamięci. Znaczenie może mieć wydajność kontenera DI, ale Nette DI kompiluje kontener do zwykłego kodu PHP, co daje praktycznie zerowy narzut podczas działania aplikacji.
Pisząc kod zgodnie z zasadami DI, często musisz tworzyć konstruktory przyjmujące zależności. Kiedyś mogło się to wydawać żmudne, ale nowoczesne IDE i możliwości takie jak promocja właściwości w konstruktorze z PHP 8 sprawiają, że jest to bardzo szybkie. Fabryki często potrafi wygenerować automatycznie Nette DI, co dodatkowo ogranicza powtarzalny kod. Z drugiej strony pozbywasz się konieczności pisania singletonów i statycznych akcesorów.
Ogólnie dobrze zaprojektowana aplikacja używająca DI jest zwykle ani znacząco krótsza, ani dłuższa niż taka opierająca się na singletonach czy dostępie globalnym. Kod związany z tworzeniem i łączeniem zależności po prostu przenosi się z poszczególnych klas do dedykowanych miejsc: konfiguracji kontenera DI i fabryk.
Jak przepisać starą aplikację na DI?
Migracja starej aplikacji na wstrzykiwanie zależności może być wymagającym procesem, zwłaszcza przy dużych i złożonych aplikacjach. Ważne, aby podejść do tego procesu systematycznie.
- Przy przechodzeniu na wstrzykiwanie zależności ważne jest, aby wszyscy członkowie zespołu rozumieli używane zasady i praktyki.
- Najpierw przeanalizuj istniejącą aplikację, aby zidentyfikować kluczowe komponenty i ich zależności. Utwórz plan, które części będą refaktoryzowane i w jakiej kolejności.
- Zaimplementuj kontener DI albo, lepiej, użyj istniejącej biblioteki, takiej jak Nette DI.
- Stopniowo refaktoryzuj części aplikacji, aby używały wstrzykiwania zależności. Może to obejmować modyfikowanie konstruktorów albo metod tak, aby przyjmowały zależności jako parametry.
- Zaktualizuj kod, w którym tworzone są instancje obiektów, tak aby pobierał je z kontenera albo używał fabryk dostarczanych przez kontener.
Pamiętaj, że przejście na wstrzykiwanie zależności to inwestycja w jakość kodu i długoterminową utrzymywalność aplikacji. Choć wprowadzenie tych zmian może być trudne, wynikiem powinien być czystszy, bardziej modułowy i łatwo testowalny kod, gotowy na przyszłe rozszerzenia i utrzymanie.
Dlaczego kompozycja jest preferowana nad dziedziczeniem?
Używanie kompozycji jest zwykle preferowane nad dziedziczeniem przy ponownym wykorzystaniu kodu, bo prowadzi do luźniejszego powiązania. Przy kompozycji rzadziej napotkasz problemy, w których zmiana klasy bazowej psuje zależne podklasy. Typowym przykładem jest sytuacja określana jako piekło konstruktorów.
Czy Nette DI Container da się używać poza Nette?
Jak najbardziej. Nette DI Container jest częścią Nette, ale zaprojektowano go jako samodzielną bibliotekę, której można używać niezależnie od pozostałych części frameworka. Wystarczy zainstalować ją przez Composera, utworzyć plik konfiguracyjny definiujący Twoje usługi, a następnie kilkoma wierszami kodu PHP utworzyć kontener DI. I możesz od razu zacząć korzystać z zalet wstrzykiwania zależności w swoich projektach.
Rozdział o Nette DI Container opisuje konkretny przypadek użycia wraz z przykładami kodu.
Dlaczego konfiguracja jest w plikach NEON?
NEON to prosty i łatwy do czytania język konfiguracyjny opracowany w ramach Nette do konfigurowania aplikacji, usług i ich zależności. W porównaniu z JSON-em czy YAML-em oferuje do tego celu o wiele bardziej intuicyjne i elastyczne możliwości. W NEON-ie możesz naturalnie opisać definicje usług i relacje, których w JSON-ie czy YAML-u trudno albo w ogóle nie da się wyrazić równie przejrzyście.
Czy parsowanie plików NEON spowalnia aplikację?
Choć pliki NEON parsują się bardzo szybko, szybkość ich parsowania jest na produkcji w zasadzie bez znaczenia. Pliki konfiguracyjne parsowane są bowiem tylko raz, przy pierwszym uruchomieniu aplikacji (albo gdy się zmienią). Po sparsowaniu generowany jest kod kontenera DI, zapisywany do cache (na dysk), a ten skompilowany kod PHP wykonywany jest przy każdym kolejnym żądaniu, co eliminuje potrzebę dalszego parsowania.
Tak to działa w środowisku produkcyjnym. Podczas tworzenia aplikacji pliki NEON parsowane są za każdym razem, gdy zmieni się ich treść, dzięki czemu programista ma zawsze aktualny kontener DI. Jak wspomniano, samo parsowanie jest bardzo szybkie.
Jak sięgnąć w klasie po parametry z pliku konfiguracyjnego?
Pamiętaj o zasadzie nr 1: pozwól, aby Ci to przekazano. Jeśli klasa potrzebuje informacji z pliku konfiguracyjnego, nie kombinuj, jak może je pobrać. Zamiast tego po prostu o nie poproś, na przykład przez konstruktor klasy. A potem podaj tę wartość w pliku konfiguracyjnym.
W tym przykładzie %myParameter% to symbol zastępczy wartości parametru myParameter, która
zostanie przekazana do konstruktora MyClass:
# config.neon
parameters:
myParameter: Some value
services:
- MyClass(%myParameter%)
Jeśli chcesz przekazać wiele parametrów albo użyć autowiringu, warto opakować parametry w obiekt.
Czy Nette obsługuje interfejs Container z PSR-11?
Nette DI Container nie obsługuje PSR-11 bezpośrednio. Jeśli jednak potrzebujesz interoperacyjności między Nette DI Container a bibliotekami czy frameworkami oczekującymi interfejsu Container z PSR-11, możesz utworzyć prosty adapter, który posłuży jako most między Nette DI Container a PSR-11.
Co oznaczają terminy container, compiler, definition itd.?
Krótki słowniczek słów, które nieustannie pojawiają się wokół Nette DI, w większości przy pisaniu rozszerzeń:
- Container – skompilowany obiekt (
Nette\DI\Container), który tworzy usługi na żądanie i trzyma je w czasie działania. Generowany jest raz jako zoptymalizowany kod PHP. - Compiler – maszyneria zamieniająca pliki konfiguracyjne i rozszerzenia w tę klasę kontenera.
- ContainerBuilder – zmienny model kontenera używany podczas kompilacji; trzyma definicje usług, zanim powstanie jakakolwiek prawdziwa usługa. Zobacz Tworzenie rozszerzeń.
- Service – obiekt zarządzany przez kontener, zwykle tworzony raz i współdzielony (singleton) – połączenie z bazą danych, mailer, logger.
- Definition – przepis na usługę: jej typ, sposób utworzenia i to, co zrobić potem. Nette zamienia definicje w metody fabryczne kontenera; istnieje kilka rodzajów (zobacz typy definicji).
- Type – klasa albo interfejs usługi, używany przez autowiring do dopasowywania usług do miejsc, które ich wymagają.
- Autowiring – automatyczne przekazywanie usług do konstruktorów i metod według ich typu, dzięki czemu nie łączysz zależności ręcznie.
- Tag – etykieta dołączona do definicji (opcjonalnie z wartością); rozszerzenie może potem znaleźć wszystkie
usługi ją noszące przez
findByTag(). - Setup – dodatkowe wywołania wykonywane na usłudze tuż po jej utworzeniu – wywołania metod albo przypisania do
właściwości, dodawane przez
addSetup(). - Alias – alternatywna nazwa istniejącej usługi.