Часто задаваемые вопросы о DI (FAQ)
- DI – это другое название IoC?
- Что такое Service Locator?
- Когда лучше не использовать DI?
- Есть ли у DI недостатки?
- Как переписать старое приложение на DI?
- Почему композиция предпочтительнее наследования?
- Можно ли использовать Nette DI Container вне Nette?
- Почему конфигурация в файлах NEON?
- Не замедляет ли разбор файлов NEON приложение?
- Как обратиться в своём классе к параметрам из конфигурационного файла?
- Поддерживает ли Nette интерфейс контейнера PSR-11?
- Что означают термины container, compiler, definition и прочие?
DI – это другое название IoC?
Inversion of Control (IoC) – принцип, описывающий поток управления в
программе: ваш код вызывает внешний код или внешний код (например,
фреймворк) вызывает ваш? IoC – широкое понятие, охватывающее события, так называемый голливудский принцип и
другие аспекты. К этому понятию относятся и фабрики, о которых
говорится в разделе Правило №
3: пусть этим займётся фабрика, представляющие собой инверсию
оператора new.
Dependency Injection (DI) сосредоточен на том, как объекты получают свои зависимости (то есть другие объекты, с которыми им нужно работать). Это шаблон проектирования, призывающий передавать зависимости объектам явно, а не заставлять объекты создавать или разыскивать их.
Поэтому DI можно считать частным случаем IoC. Однако не все формы IoC способствуют чистоте кода. Например, антипаттернами являются приёмы, опирающиеся на глобальное состояние или на шаблон Service Locator.
Что такое Service Locator?
Это альтернативный подход к Dependency Injection. Он состоит в том, что есть центральный объект (локатор), в котором зарегистрированы все доступные сервисы (зависимости). Когда объекту нужна зависимость, он запрашивает её у Service Locator.
Однако по сравнению с DI ему не хватает прозрачности. Зависимости спрятаны внутри кода объекта (в вызовах локатора), а не выражены явно в его API (в конструкторе или методах), поэтому, чтобы разобраться в связях, приходится читать код. Тестирование тоже усложняется: вы не можете просто передать mock-зависимости при создании объекта, часто приходится вмешиваться в сам Service Locator. Кроме того, Service Locator вносит лишнюю зависимость: объекты становятся связаны с локатором, тогда как при DI объекты в идеале о контейнере ничего не знают.
Когда лучше не использовать DI?
Известных существенных недостатков правильного использования шаблона Dependency Injection нет. Наоборот, получение зависимостей из глобально доступных мест (таких как статические свойства или синглтоны) приводит к множеству осложнений, как и использование Service Locator. Поэтому использовать DI, как правило, всегда уместно. Это не догма; просто ничего лучшего для чистого управления зависимостями пока широко не прижилось.
Однако есть отдельные, ограниченные ситуации, когда глобальное обращение к объектам может быть допустимо. Например, при отладке, когда нужно вывести значение переменной, измерить время выполнения или записать сообщение в определённом месте. В этих случаях, связанных с временными действиями, которые позже уберут из кода, использование глобально доступного дампера, таймера или логгера может быть законным. Эти инструменты не входят в основную архитектуру приложения.
Есть ли у DI недостатки?
Влечёт ли использование Dependency Injection недостатки вроде увеличения объёма кода или снижения производительности? Что мы теряем, начиная писать код в соответствии с DI?
Сам DI пренебрежимо влияет на производительность во время выполнения и на расход памяти. Производительность DI-контейнера может иметь значение, но Nette DI компилирует контейнер в обычный PHP-код, поэтому накладные расходы при выполнении приложения практически нулевые.
Когда вы пишете код по принципам DI, вам часто приходится создавать конструкторы, принимающие зависимости. Раньше это могло казаться утомительным, но современные IDE и такие возможности, как продвижение свойств конструктора в PHP 8, делают это очень быстрым. Фабрики Nette DI часто может генерировать автоматически, что ещё сильнее сокращает шаблонный код. С другой стороны, отпадает необходимость писать синглтоны и статические аксессоры.
В целом хорошо спроектированное приложение с DI обычно не заметно короче и не заметно длиннее того, что опирается на синглтоны или глобальный доступ. Код, отвечающий за создание и связывание зависимостей, просто переезжает из отдельных классов в специально отведённые места: конфигурацию DI-контейнера и фабрики.
Как переписать старое приложение на DI?
Переход старого приложения на Dependency Injection может оказаться непростым, особенно для больших и сложных приложений. Важно подойти к этому процессу систематически.
- При переходе на Dependency Injection важно, чтобы все члены команды понимали используемые принципы и практики.
- Сначала проанализируйте существующее приложение, чтобы выявить ключевые составляющие и их зависимости. Составьте план, какие части и в каком порядке будут переработаны.
- Реализуйте DI-контейнер или, что лучше, используйте существующую библиотеку, например Nette DI.
- Постепенно переводите части приложения на Dependency Injection. Это может означать изменение конструкторов или методов так, чтобы они принимали зависимости параметрами.
- Обновите код в местах создания объектов, чтобы получать их из контейнера или использовать фабрики, предоставляемые контейнером.
Помните, что переход на Dependency Injection – это вложение в качество кода и долгосрочную поддерживаемость приложения. Внести эти изменения может быть непросто, но результатом должен стать более чистый, модульный и легко тестируемый код, готовый к будущим расширениям и поддержке.
Почему композиция предпочтительнее наследования?
Для переиспользования кода композиция обычно предпочтительнее наследования, потому что она даёт более слабую связанность. При композиции вы реже сталкиваетесь с тем, что изменение базового класса ломает зависимые подклассы. Типичный пример – ситуация, которую называют адом конструкторов.
Можно ли использовать Nette DI Container вне Nette?
Разумеется. Nette DI Container входит в состав Nette, но спроектирован как самостоятельная библиотека, которую можно использовать независимо от других частей фреймворка. Достаточно установить её через Composer, создать конфигурационный файл с описанием ваших сервисов, а затем несколькими строками PHP-кода создать DI-контейнер. И вы сразу можете начать пользоваться преимуществами Dependency Injection в своих проектах.
В главе Nette DI Container описан конкретный сценарий использования с примерами кода.
Почему конфигурация в файлах NEON?
NEON – простой и легко читаемый язык конфигурации, разработанный внутри Nette для настройки приложений, сервисов и их зависимостей. По сравнению с JSON или YAML он даёт для этой цели куда более интуитивные и гибкие возможности. В NEON можно естественно описать определения сервисов и связи, которые в JSON или YAML выразить так же наглядно было бы трудно или невозможно.
Не замедляет ли разбор файлов NEON приложение?
Хотя файлы NEON разбираются очень быстро, скорость их разбора в продакшене по большому счёту не важна. Дело в том, что конфигурационные файлы разбираются только один раз, при первом запуске приложения (или при их изменении). После разбора порождается код DI-контейнера, он кешируется (сохраняется на диск), и при каждом последующем запросе выполняется уже этот скомпилированный PHP-код, так что повторный разбор не нужен.
Так это работает в производственной среде. Во время разработки файлы NEON разбираются каждый раз при изменении их содержимого, благодаря чему у разработчика всегда актуальный DI-контейнер. Как уже сказано, сам разбор происходит очень быстро.
Как обратиться в своём классе к параметрам из конфигурационного файла?
Помните Правило № 1: пусть вам это передадут. Если классу нужны сведения из конфигурационного файла, не пытайтесь придумать, как класс может их раздобыть. Вместо этого просто запросите их, например через конструктор класса. А затем передайте это значение в конфигурационном файле.
В этом примере %myParameter% – подстановка значения параметра
myParameter, которое будет передано в конструктор MyClass:
# config.neon
parameters:
myParameter: Some value
services:
- MyClass(%myParameter%)
Если вы хотите передать несколько параметров или использовать autowiring, полезно упаковать параметры в объект.
Поддерживает ли Nette интерфейс контейнера PSR-11?
Nette DI Container не поддерживает PSR-11 напрямую. Однако если вам нужна совместимость Nette DI Container с библиотеками или фреймворками, ожидающими интерфейс контейнера PSR-11, вы можете создать простой адаптер, который послужит мостом между Nette DI Container и PSR-11.
Что означают термины container, compiler, definition и прочие?
Краткий словарь слов, которые постоянно встречаются вокруг Nette DI, чаще всего при написании расширений:
- Container (контейнер) – скомпилированный объект (
Nette\DI\Container), который создаёт сервисы по требованию и держит их во время выполнения. Он порождается однажды в виде оптимизированного PHP-кода. - Compiler (компилятор) – механизм, который превращает конфигурационные файлы и расширения в этот класс контейнера.
- ContainerBuilder – изменяемая модель контейнера, используемая во время компиляции; она хранит определения сервисов до того, как появится хотя бы один настоящий сервис. См. Создание расширений.
- Service (сервис) – объект, управляемый контейнером, обычно создаваемый однажды и общий для всех (синглтон): соединение с базой данных, сервис отправки почты, логгер.
- Definition (определение) – рецепт сервиса: его тип, способ создания и то, что нужно сделать после. Nette превращает определения в фабричные методы контейнера; видов определений несколько (см. типы определений).
- Type (тип) – класс или интерфейс сервиса, который autowiring использует, чтобы сопоставить сервисы с местами, где они нужны.
- Autowiring – автоматическая передача сервисов в конструкторы и методы по их типу, чтобы вам не приходилось связывать зависимости вручную.
- Tag (тег) – метка, прикреплённая к определению (при желании со
значением); расширение затем может найти все сервисы с этой меткой
через
findByTag(). - Setup (настройка) – дополнительные действия над сервисом сразу
после его создания: вызовы методов или присваивание свойств,
добавляемые через
addSetup(). - Alias (псевдоним) – альтернативное имя существующего сервиса.