diff --git a/guide/ru/concept/di-container.md b/guide/ru/concept/di-container.md new file mode 100644 index 0000000..4c9a841 --- /dev/null +++ b/guide/ru/concept/di-container.md @@ -0,0 +1,209 @@ +# Внедрение зависимостей и контейнер внедрения зависимостей + +## Внедрение зависимостей + +В ООП существует два способа повторного использования кода: наследование и композиция. + +Наследование - это просто: + +```php +class Cache +{ + public function getCachedValue($key) + { + // .. + } +} + +class CachedWidget extends Cache +{ + public function render(): string + { + $output = $this->getCachedValue('cachedWidget'); + if ($output !== null) { + return $output; + } + // ... + } +} +``` + +Проблема здесь в том, что эти два класса становятся излишне сопряженными или взаимозависимыми, что делает их более хрупкими. + +Есть способ справиться с этой проблемой — композиция: + +```php +interface CacheInterface +{ + public function getCachedValue($key); +} + +final class Cache implements CacheInterface +{ + public function getCachedValue($key) + { + // .. + } +} + +final class CachedWidget +{ + private CacheInterface $cache; + + public function __construct(CacheInterface $cache) + { + $this->cache = $cache; + } + + public function render(): string + { + $output = $this->cache->getCachedValue('cachedWidget'); + if ($output !== null) { + return $output; + } + // ... + } +} +``` + +Мы избежали ненужного наследования и использовали интерфейс, чтобы уменьшить сопряженность. +Вы можете заменить реализацию кэша без изменения класса `CachedWidget`, поэтому он становится более стабильным. + +Здесь `CacheInterface` это зависимость - объект, от которого зависит другой объект. +Процесс помещения экземпляра объекта зависимости в объект (`CachedWidget`) называется внедрением зависимости. + +Существует множество способов его реализации: + +- Внедрение через конструктор. Лучше всего подходит для обязательных зависиомостей. +- Через метод. Лучше использовать для необязательных зависимостей. +- Через свойство. Лучше избегать использования в PHP, за исключением, может быть, объектов передачи данных (DTO) + +## Контейнер внедрения зависимостей + +Внедрять базовые зависимости просто и легко. +Вы выбираете место где вас не волнуют зависимости, которые обычно являются обработчиками действий и которые вы не собираетесь тестировать, создаете экземпляры необходимых зависимостей и передаете их в зависимые классы. + +Это хорошо работает, когда в целом зависимостей немного и нет вложенных зависимостей. +Когда их много, и каждая зависимость сама имеет зависимости, создание всей иерархии становится утомительным процессом, который требует большого количества кода и может привести к трудно отлаживаемым ошибкам. + +Кроме того, многие зависимости, такие как некоторые сторонние обертки API, одинаковы для любого класса, использующего его. +Поэтому имеет смысл: + +- Определить, как создать экземпляр такой обертки API один раз. +- Создавать его экземпляр при необходимости и только один раз за запрос. + +Именно для этого нужны контейнеры зависимостей. + +Контейнер внедрения зависимостей (DI-контейнер) - это объект, который знает, как создавать и настраивать объекты и все зависимые от них объекты. +[Статья Мартина Фаулера](https://martinfowler.com/articles/injection.html) хорошо объясняет почему DI-контейнер полезен. +Здесь мы в основном поясним использование DI-контейнера, предоставляемого Yii. + +Yii реализует DI-контейнер через пакет [yiisoft/di](https://github.com/yiisoft/di) и [yiisoft/injector](https://github.com/yiisoft/injector). + +### Конфигурирование контейнера + +Поскольку для создания нового объекта вам нужны его зависимости, вам следует зарегестрировать их как можно раньше. +Вы можете сделать это в конфигурации приложения, `config/web.php`. +Например, для следующего сервиса: + +```php +class MyService implements MyServiceInterface +{ + public function __construct(int $amount) + { + } + + public function setDiscount(int $discount): void + { + + } +} +``` + +конфигурация может быть: + +```php +return [ + MyServiceInterface::class => [ + 'class' => MyService::class, + '__construct()' => [42], + 'setDiscount()' => [10], + ], +]; +``` + +Это соответствует: + +```php +$myService = new MyService(42); +$myService->setDiscount(10); +``` + +Существуют дополнительные методы объявления зависимостей: + +```php +return [ + // объявить класс для интерфейса, автоматически разрешить зависимости + EngineInterface::class => EngineMarkOne::class, + + // определение в массиве (то же, что и выше) + 'full_definition' => [ + 'class' => EngineMarkOne::class, + '__construct()' => [42], + '$propertyName' => 'value', + 'setX()' => [42], + ], + + // замыкание + 'closure' => static function(ContainerInterface $container) { + return new MyClass($container->get('db')); + }, + + // статический вызов + 'static_call' => [MyFactory::class, 'create'], + + // экземпляр объекта + 'object' => new MyClass(), +]; +``` + +### Внедрение зависимостей + +Непосредственное обращение к контейнеру в классе - плохая идея, так как код становится неуниверсальным, сопряжен с интерфейсом контейнера и, что еще хуже, зависимости становятся скрытыми. + +Поэтому Yii инвертирует управление, автоматически вводя объекты из контейнера в конструкторы и методы, основываясь на типах аргументов. + +В основном это делается в конструкторе и методе, обрабатывающем действие: + +```php +use \Yiisoft\Cache\CacheInterface; + +class MyController +{ + private CacheInterface $cache; + + public function __construct(CacheInterface $cache) { + $this->cache = $cache; + } + + public function actionDashboard(RevenueReport $report) + { + $reportData = $this->cache->getOrSet('revenue_report', function() use ($report) { + return $report->getData(); + }); + + return $this->render('dashboard', [ + 'reportData' => $reportData, + ]); + } +} +``` + +Поскольку именно [yiisoft/injector](https://github.com/yiisoft/injector) создает экземпляр и вызывает обработчик действий - он проверяет типы аргументов конструктора и метода, получает зависимости этих типов из контейнера и передает их как аргументы. +Обычно это называется автоматическим разрешением зависимостей. +Это происходит и с дополнительными зависимостями - если вы явно не указываете зависимость, контейнер сначала проверит, есть ли у него такая зависимость. +Достаточно объявить нужную вам зависимость, и она будет получена из контейнера автоматически. + +## Полезные ссылки + +- [Inversion of Control Containers and the Dependency Injection pattern Мартина Фаулера](https://martinfowler.com/articles/injection.html)