В Symfony2, почему плохая идея вводить контейнер обслуживания, а не отдельные сервисы?
Я не могу найти ответ на этот вопрос...
Если я вставляю контейнер службы, например:
// config.yml
my_listener:
class: MyListener
arguments: [@service_container]
my_service:
class: MyService
// MyListener.php
class MyListener
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function myFunction()
{
$my_service = $this->container->get('my_service');
$my_service->doSomething();
}
}
то он работает так же хорошо, как если бы я делал:
// config.yml
my_listener:
class: MyListener
arguments: [@my_service]
my_service:
class: MyService
// MyListener.php
class MyListener
{
protected $my_service;
public function __construct(MyService $my_service)
{
$this->my_service = $my_service;
}
public function myFunction()
{
$this->my_service->doSomething();
}
}
Итак, почему я не должен просто вводить контейнер службы и получать службы от этого внутри моего класса?
Ответы
Ответ 1
Мой список причин, по которым вы должны предпочесть инъекционные услуги:
-
Ваш класс зависит только от служб, которые ему нужны, а не от контейнера службы. Это означает, что услугу можно использовать в среде, которая не использует контейнер обслуживания Symfony. Например, вы можете превратить свою службу в библиотеку, которая может быть использована в Laravel, Phalcon и т.д. - ваш класс не знает, как впрыскиваются зависимости.
-
Определяя зависимости на уровне конфигурации, вы можете использовать компоновщик конфигурации, чтобы узнать, какие службы используют другие службы. Например, вводя @mailer
, тогда довольно легко выработать из контейнера службы, в который была отправлена почтовая программа. С другой стороны, если вы делаете $container->get('mailer')
, то в значительной степени единственный способ узнать, где используется почтовая программа, - это сделать find
.
-
Вы будете уведомлены о недостающих зависимостях при компиляции контейнера, а не во время выполнения. Например, представьте, что вы определили службу, которую вы вводите в слушатель. Несколько месяцев спустя вы случайно удалите конфигурацию службы. Если вы вводите услугу, вы будете уведомлены, как только вы очистите кеш. Если вы введете контейнер службы, вы обнаружите ошибку, только когда слушатель потерпит неудачу из-за того, что контейнер не может получить услугу. Несомненно, вы могли бы забрать это, если у вас есть тщательное интеграционное тестирование, но... у вас есть тщательное интеграционное тестирование, не так ли?;)
-
Вы узнаете, скорее, если вы введете неправильную услугу. Например, если у вас есть:
public function __construct(MyService $my_service)
{
$this->my_service = $my_service;
}
Но вы определили слушателя как:
my_listener:
class: Whatever
arguments: [@my_other_service]
Когда слушатель получает MyOtherService
, тогда PHP выдает ошибку, сообщая вам, что он получает неправильный класс. Если вы делаете $container->get('my_service')
, вы предполагаете, что контейнер возвращает правильный класс, и это может занять много времени, чтобы понять, что его "нет".
-
Если вы используете IDE, тогда введите hinting добавьте дополнительную дополнительную помощь. Если вы используете $service = $container->get('service');
, тогда ваша среда IDE не знает, что такое $service
. Если вы вводите
public function __construct(MyService $my_service)
{
$this->my_service = $my_service;
}
то ваша среда IDE знает, что $this->my_service
является экземпляром MyService
и может предлагать помощь с именами методов, параметрами, документацией и т.д.
-
Ваш код легче читать. Все ваши зависимости определяются прямо там, в верхней части класса. Если они разбросаны по всему классу с помощью $container->get('service')
, то это может быть намного сложнее выяснить.
-
Ваш код проще unit test. Если вы вводите контейнер обслуживания, вы должны издеваться над контейнером сервиса и настраивать макет для возврата mocks соответствующих сервисов. Внедряя услуги напрямую, вы просто издеваетесь над услугами и вводите их - вы пропускаете целый уровень сложности.
-
Не обманывайтесь ошибкой "он допускает ленивую загрузку". Вы можете настроить ленивую загрузку на уровне конфигурации, просто отметив службу как lazy: true
.
Лично единственный раз, когда инъекция контейнера службы была наилучшим возможным решением, я пытался внедрить контекст безопасности в приемник доктрины. Это вызывало циклическое исключение ссылки, поскольку пользователи были сохранены в базе данных. В результате доктрина и контекст безопасности зависели друг от друга во время компиляции. Введя контейнер обслуживания, я смог обойти круговую зависимость. Однако это может быть запах кода, и есть способы обойти его (например, с помощью диспетчера событий), но я признаю добавленный осложнение может перевесить преимущества.
Ответ 2
Это не очень хорошая идея, потому что вы делаете свой класс зависимым от DI. Что произойдет, когда когда-нибудь вы решите вытащить свой класс и использовать его в совершенно другом проекте? Теперь я не говорю о Symfony или даже PHP, я говорю вообще. Поэтому в этом случае вы должны убедиться, что новый проект использует один и тот же механизм DI с теми же методами, которые поддерживаются, или вы получаете исключения. И что произойдет, если проект вообще не использует DI, или использует какую-то прохладную новую реализацию DI? Вы должны пройти всю вашу кодовую базу и изменить все, чтобы поддержать новый DI. В крупных проектах это может быть проблематичным и дорогостоящим, особенно когда вы занимаете не только один класс.
Лучше всего сделать ваши классы как можно более независимыми. Это означает, что DI из вашего обычного кода, что-то вроде третьего человека, который решает, что происходит, указывает, куда идти, но не идет туда и делает это сам. Вот как я это понимаю.
Хотя, как сказал tomazahlin, я согласен, что в проектах Symfony в редких случаях это помогает предотвратить круговые зависимости. Это единственный пример, где я буду использовать его, и я уверен, что это единственный вариант.
Ответ 3
Помимо всех недостатков, объясняемых другими (отсутствие контроля над используемыми службами, компиляция времени выполнения, отсутствие зависимостей и т.д.)
Существует одна основная причина, которая нарушает основное преимущество использования DIC - Замена зависимостей.
Если служба определена в библиотеке, вы не сможете заменить ее зависимыми от локальных, которые удовлетворяют ваши потребности.
Только эта причина достаточно сильна, чтобы не вводить всю DIC. Вы просто нарушаете идею замены зависимостей, так как они HARDCODED! в службе;)
BTW. Не забудьте потребовать interfaces
в конструкторе службы вместо определенных классов столько, сколько вы можете - снова замена хороших зависимостей.
EDIT: пример замены заметок
Определение службы у какого-либо поставщика:
<service id='vendor_service' class="My\VendorBundle\SomeClass" />
<argument type="service" id="vendor_dependency" />
</service>
Замена в приложении:
<service id='vendor_service' class="My\VendorBundle\SomeClass" />
<argument type="service" id="app_dependency" />
</service>
Это позволяет заменить логику поставщика вашим настроенным, но не забудьте реализовать требуемый интерфейс класса. С жестко закодированными зависимостями вы не можете заменить зависимость в одном месте.
Вы также можете переопределить службу vendor_dependency
, но это заменит ее во всех местах не только в vendor_service
.
Ответ 4
Ввод всего контейнера не является хорошей идеей в целом. Ну, это работает, но зачем вводить весь контейнер, в то время как вам нужно только несколько других сервисов или параметров.
Иногда вы хотите ввести весь контейнер, чтобы избежать циклических ссылок, потому что если вы введете весь контейнер, вы получите "ленивую загрузку" требуемых услуг. Примером могут быть слушатели сущности doctrine.
Вы можете получить контейнер из каждого класса, который является "Container Aware" или имеет доступ к ядру.