Выпуск Symfony 2.0 в 2011 году, вероятно, является самым важным шагом на пути к принятию шаблона проектирования внедрения зависимостей в мире PHP. Предоставляя реальный вариант использования этого шаблона в признанной и популярной среде PHP, многие разработчики начали внедрять внедрение зависимостей в свои проекты.

В 2018 году использование внедрения зависимостей в PHP получило широкое распространение: почти все современные PHP-фреймворки предоставляют контейнер DI, а группа взаимодействия PHP Framework даже создала стандарт по этому вопросу.

В этой статье и в дальнейшем я хотел бы обсудить причины, по которым внедрение зависимостей имеет значение, и познакомить вас с внутренним устройством компонента Symfony 4 Dependency Injection, чтобы лучше понять, как использовать его функции.

Что такое внедрение зависимостей?

Прежде чем говорить о том, как реализовать DI, давайте поговорим о самом шаблоне, чтобы убедиться, что мы согласны с тем, почему он полезен.

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

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

class Persister {
    // A class able to persist a user
}
class Mailer {
    // A class able to send an e-mail to the user
}
class RegistrationManager {
    private $persister;
    private $mailer;
    public function __construct(Persister $p, Mailer $m) {
        $this->persister = $p;
        $this->mailer = $m;
    }
    public function register(User $user) {
        // Register the user using our injected classes:
        // $this->persister->persist($user);
        // $this->mailer->sendConfirmation($user);
    }
}

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

Одним из ключевых преимуществ инкапсуляции является возможность легко тестировать каждый компонент независимо, вводя в него разные объекты вместо реальных (мы могли бы, например, внедрить другой Persister, который на самом деле ничего не сохраняет, чтобы для тестирования RegistrationManager без использования базы данных). Идея внедрения поддельных объектов в объект, который мы хотим протестировать, называется издевательством.

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

Однако, хотя использование инкапсуляции является удивительным улучшением по сравнению с процедурным кодом с точки зрения разделения, у нее все еще есть недостаток: пользователю класса необходимо вручную вводить в него зависимости. В этом конкретном случае пользователю, желающему использовать RegistrationManager, сначала потребуется создать Persister и Mailer:

$manager = new RegistrationManager(
    new Persister($doctrine),
    new Mailer($twig, $swiftmailer)
);

Это создает две новые основные проблемы:

  • писать так много new как пользователю класса может стать обременительным (этот пример небольшой, но он может быстро стать намного больше для многих классов);
  • что еще более важно, возникает проблема ответственности: передавая ответственность за внедрение правильных зависимостей класса своему пользователю, разработчик, поддерживающий класс, не может добавлять или удалять зависимости для этого класса. В нашем примере, если RegistrationManager потребовалась новая зависимость (т. Е. Если в его конструктор был добавлен новый аргумент), каждое использование этого класса в коде приложения необходимо будет обновить, чтобы внедрить эта новая зависимость;

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

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

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

$manager = new RegistrationManager(
    new Persister($doctrine),
    new Mailer($twig, $swiftmailer)
);

Эта универсальная система также предоставит пользователю возможность получить экземпляр класса без необходимости иметь дело с его зависимостями.

Что такое сервисный контейнер?

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

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

Примечание: когда вы используете контейнер службы, все классы, которые вы можете создать с его помощью, называются службами, но они по-прежнему являются простыми классами (это просто другое имя).

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

В Symfony компонент DependencyInjection - это тот, который предоставляет этот контейнер, считывает конфигурацию из пакетов и создает для вас экземпляры служб.

Как создать простой сервисный контейнер?

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

Как разработчику сервисного контейнера нам необходимо предоставить две ключевые функции:

  • способ для разработчиков зарегистрировать способ создания экземпляров своих классов;
  • способ для пользователей нашего контейнера получить созданные экземпляры классов;

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

$container = new Container();
// Define how to create services instances using anonymous functions
$container['session_storage'] = function ($c) {
    return new SessionStorage('SESSION_ID');
};

$container['session'] = function ($c) {
    return new Session($c['session_storage']);
};
// Use the container to retrieve the session: note how we didn't
// need to know the dependency of Session on SessionStorage
$user = $container['session']->get('user');

Этот чрезвычайно простой контейнер на самом деле существует в экосистеме Symfony: он называется Pimple и использовался ныне устаревшим микро-фреймворком Silex.

Примечание: есть даже контейнер, вмещающий 140 символов, Twitee :)!

Предстоящие

В ближайшие недели я опубликую продолжение этой статьи, чтобы немного глубже изучить внедрение зависимостей и то, как реализовать расширенные функции, используя в качестве примера внутреннее устройство компонента Symfony Dependency Injection!

Есть ли у вас отзывы / идеи? Не стесняйтесь комментировать!