Типы внедрения

Дата обновления перевода 2024-07-27

Типы внедрения

Сделать зависимости класса ясными и требовать, чтобы они были внедрены в него - это хороший способ сделать класс более пригодным к повторному использованию, тестированию и отделённому от других.

Существует несколько способов того, как могут быть внедрены зависимости. Каждая точка внедрения имеет свои преимущества и недостатки, которые стоит учесть, так же как и разные способы работы с ними при использовании сервис-контейнера.

Внедрение конструктора

Наиболее распространённый способ внедрить зависимости - через конструктор класса. Чтобы сделать это, вам нужно добавить аргумент к подписи конструктора, чтобы принять зависимость:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Mail/NewsletterManager.php
namespace App\Mail;

// ...
class NewsletterManager
{
    public function __construct(
        private MailerInterface $mailer,
    ) {
    }

    // ...
}

Вы можете указать, какой сервис вы хотите внедрить в него в конфигурации сервис-контейнера:

1
2
3
4
5
6
# config/services.yaml
services:
    # ...

    App\Mail\NewsletterManager:
        arguments: ['@mailer']

Tip

Типизирование внедрённого объетка означает, что вы можете быть уверены, что была внедрена подходящая зависимость. Путём типизирования вы получите чёткую ошибку сразу же, если внедряется неподходящая зависимость. Типизируя, используя интерфейс, а не класс, вы делаете выбор зависимости более гибким. И если предположить, что вы используете толко методы, определённые в интерфейсе, вы можете получить эту гибкость и всё равно безопасно использовать объект.

Существует несколько преимуществ использования конструкторного внедрения:

  • Если зависимость является требованием и класс не может без неё работать, тогда внедрение через конструктор гарантирует, что она будет присутствовать, когда будет использован класс, так каккласс не может быть создан без неё.
  • Конструктор вызывается только один раз, когда создаётся объект, так что вы можете быть уверены, что зависимость не изменится во время жизненного цикла объекта.

Эти преимущества означают, что конструкторное внедрение не подходит для работы с необязательными зависимостями. Также его сложнее использовать в комбинации с иерархией классов: если класс использует конструкторное внедрение, то расширение и переопределения конструктора становятся проблематичны.

Внедрение неизменяемого сеттера

Другим возможным внедрением является использование метода, который возвращает отдельный экземпляр клонируя исходный сервис, такой подход позволяет сделать сервис неизменяемым:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/Mail/NewsletterManager.php
namespace App\Mail;

// ...
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Contracts\Service\Attribute\Required;

class NewsletterManager
{
    private MailerInterface $mailer;

    /**
     * @return static
     */
    #[Required]
    public function withMailer(MailerInterface $mailer): self
    {
        $new = clone $this;
        $new->mailer = $mailer;

        return $new;
    }

    // ...
}

Для того чтобы использовать этот тип внедрения, не забудьте сконфигурировать его:

1
2
3
4
5
6
7
8
# config/services.yaml
services:
     # ...

     app.newsletter_manager:
         class: App\Mail\NewsletterManager
         calls:
             - withMailer: !returns_clone ['@mailer']

Note

Если вы решите использовать автомонтирование, этот тип внедрения требует добавления докблока @return static для того, чтобы контейнер мог зарегистрировать метод.

Этот подход полезен, если вам нужно сконфигурировать сервис в соответствии с вашими потребностями, Итак, вот преимущества неизменяемых сеттеров:

  • Неизменяемые сеттеры работают с необязательными зависимостями, таким образом, если вам не нужна зависимость, сеттер не нужно вызывать.
  • Подобно внедрению конструктора, использование неизменяемых сеттеров заставляет зависимость оставаться неизменной в течение всего времени существования сервиса.
  • Этот тип внедрения хорошо работает с чертами, так как сервис можно компоновать, таким образом, адаптация сервиса к требованиям вашего приложения становится проще.
  • Сеттер может быть вызван несколько раз, таким образом, добавление зависимости в коллекцию становится проще и позволяет вам добавлять переменное количество зависимостей.

Недостатками являются:

  • Поскольку вызов сеттера является необязательным, зависимость может быть null при вызове методов сервиса. Вы должны проверить, что зависимость доступна, прежде чем использовать ее.
  • Если сервис не объявлен ленивым, он несовместим с сервисами, которые ссылаются друг на друга в так называемых круговых циклах.

Внедрение сеттера

Ещё одной точкой внедрения в класс является добавление сеттер-метода, который принмает зависимость:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Mail/NewsletterManager.php
namespace App\Mail;

use Symfony\Contracts\Service\Attribute\Required;

// ...
class NewsletterManager
{
    private MailerInterface $mailer;

    #[Required]
    public function setMailer(MailerInterface $mailer): void
    {
        $this->mailer = $mailer;
    }

    // ...
}
1
2
3
4
5
6
7
8
# config/services.yaml
services:
    # ...

    app.newsletter_manager:
        class: App\Mail\NewsletterManager
        calls:
            - setMailer: ['@mailer']

В этом случае преимущества такие:

  • Сеттер-внедрение хорошо работает с необязательными зависимостями. Если вам не нужна зависимость, то просто не вызывайте сеттер.
  • Вы можете вызывать сеттер несколько раз. Это особенно полезно, если метод добавляет зависимость в коллекцию. Потом у вас может быть переменное количество зависимостей.
  • Как и в случае с неизменяемым сеттером, этот тип внедрения хорошо работает с чертами и позволяет вам компоновать ваш сервис.

Недостатками сеттер-внедрения являются:

  • Сеттер может быть вызван чаще, чем просто во время построения, так что вы не можете быть уверены, что зависимость не будет заменена во время жизненного цикла объекта (разве что вы ясно не напишите сеттер- методу проверить, был ли он уже вызван).
  • Вы не можете быть уверены, что сеттер будет вызван, и поэтому вам надо добавить проверки того, были ли внедрены любые обязательные зависимости.

Внедрение свойств

Ещё одна возможность - просто установить публичные поля класса напрямую:

1
2
3
4
5
6
7
// ...
class NewsletterManager
{
    public MailerInterface $mailer;

    // ...
}
1
2
3
4
5
6
7
8
# config/services.yaml
services:
    # ...

    app.newsletter_manager:
        class: App\Mail\NewsletterManager
        properties:
            mailer: '@mailer'

В основном у внедрения свойств существуют только недостатки, оно схоже с сеттер- внедрением, но со следующими дополнительными и важными проблемами:

  • Вы вообще не можете контролировать, когда устаналивается зависимость, она может быть изенена в любой момент жизненного цикла объекта.
  • Вы не можете использовать типизирование, так что вы не можете быть уверены в том, какая зависимость была внедрена, кроме как ясно написав в код класса протестировать экземпляр, перед использованием.

Однако полезно знать, что это может быть сделано с сервис-контейнером, особенно если вы работаете с кодом, который неподвластен вам, как, например, сторонняя библиотека, которая использует публичные свойства для своих зависимостей.