Внедрение зависимостей при помощи Pimple






Разрабатывая приложения, мы пытаемся разбивать код на независимые модули, которые можно было бы использовать в работа над будущими проектами. Однако на практике это является довольно сложным делом. Зависимости между отдельными частями программы могут оказаться вполне реальным ночным кошмаром, только если вы не подходите к этому вопросу тщательно и с умом. Вот где паттерн Dependency Injection (внедрение зависимости) здорово облегчает жизнь, поскольку он позволяет внедрять зависимости в объекты непосредственно во время выполнения программного кода, исключая необходимость жёстко кодировать их непосредственно в коде.

pimple-logo


Pimple — это очень простой DI-контейнер, который предлагает использовать PHP-замыкания, чтобы внедрение зависимостей для разработчиков было как можно более простым и естественным. В этой статье мы с вами разберём проблемы, возникающие при размещении зависимостей непосредственно в коде, как их можно избежать, используя dependency injection и как в этом может помочь Pimple, позволяя создавать код, который легче поддерживать.

Проблемы при использовании конкретных зависимостей

Почти любое приложение состоит из множества классов. Один класс вызывает методы других классов, те — третьих, и так далее. Представим, что у нас есть класс A, который использует для своей работы класс B (то есть, зависит от него):

Класс А зависит от класса B потому, что если класс B или его метод b1 будет недоступен, то класс A не сможет работать.

Пойдём дальше. Всякий раз, когда вы внутри класса жестко определяете класс создаваемого объекта, вы создаёте конкретную зависимость от последнего. Конкретные зависимости — это неизбежный геморрой при написании тестов для вашего кода. Чем жёстко определять зависимости внутри класса, лучшим решением будет передавать их извне. Например, это можно сделать при помощи аргументов конструктора или специальных setter-методов.

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

Популярные сегодня сайты социальных сетей предоставляют возможность пользователям делиться контентом на своих страницах, где можно увидеть опубликованные пользователем материалы в виде ленты. Допустим, у нас есть класс SocialFeeds, который генерирует ленты из различных соцсетей вроде Twitter, Facebook, Google+ и т. п. Для работы с каждой из них используется отдельный класс. Для примера давайте рассмотрим класс TwitterService, который реализует получение данных от Twitter.

Класс SocialFeeds запрашивает данные от Twitter, используя класс TwitterService. Тот в свою очередь извлекает из БД токен пользователя для доступа к Twitter API. Полученный токен передаётся классу OAuth, который авторизуется на Twitter, получает оттуда ленту и возвращает её классу SocialFeeds.

Очевидно, что SocialFeeds зависит от TwitterService. Но TwitterService в свою очередь зависит от DB и OAuth, таким образом существует непрямая зависимость SocialFeeds от DB и OAuth.

Так в чём же суть проблемы? SocialFeeds зависит от конкретных реализаций трёх классов, таким образом невозможно выполнить отдельное юнит-тестирование SocialFeeds, не прибегая к реализации остальных классов, от которых он зависит. Или, скажем, нам понадобится использовать другую СУБД или другого провайдера OAuth. Если такое произойдёт, нам придётся вносить изменения в код везде, где создаются экземпляры соответствующих классов.

Избавление от конкретных зависимостей

Решение проблемы настолько простое, насколько простой является передача объекта другому объекту в процессе выполнения программы. Существует два способа выполнения этого: внедрение зависимости в конструкторе или при помощи setter-метода.

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

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

Экземпляр TwitterService передаётся через параметр конструктора. SocialFeeds всё ещё зависит от TwitterService, но теперь мы можем снаружи подсовывать нужную реализацию класса или даже использовать mock-объект при тестировании класса.  С классами DB and OAuthмы по ступим точно так же:

Внедрение зависимости через setter-метод

Здесь всё делается точно так же, только теперь зависимости передаются не в конструктор, а через специально для этих целей созданный метод. Например, в SocialFeeds это может быть сделано так:

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

Внедрение в конструкторе против setter-методов

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

Достоинства:

  • Конструктор. Все зависимости класса сразу видны из сигнатуры метода.
  • Setter. Добавлять новые зависимости можно в любой момент, не затрагивая существующий код.

Недостатки:

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

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

Роль Pimple во внедрении зависимостей

Вам может быть интересно, зачем нужен Pimple, если всё и так прекрасно решается пи помощи рассмотренных выше приёмов. Чтобы ответить на этот вопрос, нужно обратиться к принципу DRY, который гласит, что «Каждая часть знания должна иметь единственное, непротиворечивое и авторитетное представление в рамках системы».

Вернёмся к нашему примеру с внедрением зависимости через конструктор. Всякий раз, когда нам нужен объект класса SocialFeed, нам нужно заново создавать все необходимые объекты, чтобы передать их в конструкторы. Согласно DRY, такой повторяемости следует избегать, чтобы уменьшить количество головной боли в будущем, при сопровождении кода. Pimple выполняет роль контейнера, внутр которого выполняются все необходимые манипуляции перед подготовкой нужного объекта. Давайте взглянем на пример работы с Pimple, и вам сразу станет понятней о чём идёт речь:

Сначала создаётся экземпляр контейнера Pimple, который и будет хранить всю необходимую информацию о зависимостях и создавать их в случае необходимости. Класс Pimple реализует интерфейс SPL ArrayAccess, так что снаружи работа с контейнером Pimple напоминает работу с массивами. В следующей строке мы помещаем в контейнер имя конкретного класса, объекты которого нам понадобятся в дальнейшем.  Потом мы помещаем в контейнер функцию-замыкание, которая и выполняет всю работу по созданию объектов. Обратите внимание на параметр $c в замыкании, который является ссылкой на Pimple-контейнер и позволяет получать доступ к контейнеру изнутри замыкания. В приведённом примере замыкание извлекает из контейнера имя класса через ключ 'class_name', чтобы определить имя нужного класса при создании объекта.

Дайте переработаем наш пример из начала статьи так, чтобы использовать Pimple. На официальной странице Pimple приводится пример с использованием конструкторов, а мы в нашем примере давайте будем использовать setter-методы. Обратите внимание на то. что мы не изменяем существующий код. Всё остаётся как было, мы только инкапсулируем необходимую логику в контейнер Pimple.

Классы DB и OAuth являются независимыми, поэтому мы создаём их экземпляры непосредственно. Затем мы внедряем их в качестве зависимостей в TwitterService, используя setter-методы. Поскольку DB и OAuth уже находятся в контейнере, мы можем получить к ним доступ непосредственно из замыкания, используя вызовы $c['db'] и $c['oauth'].

Итак, теперь зависимости инкапсулированы внутри контейнера. Теперь, если вам понадобится использовать другую реализацию DB или OAuth, вы можете просто заменить их в контейнере и всё будет работать! Используя Pimple, вы можете внедрять все зависимости в одном месте вашего кода.

Продвинутое использование Pimple

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

Pimple предоставляет такую возможность. Для того необходимо всего лишь завернуть замыкание, создающее экземпляр объекта в метод share():

До сих пор мы объявляли все сервисы с их зависимостями в одном месте, внутри контейнера Pimple. Но представьте себе ситуацию, когда вам необходимо в каком-то месте программы изменить часть поведения сервиса. Например, вы захотели использовать ORM в классе TwitterService. Мы не можем менять исходный код TwitterService, поскольку это затронет все остальные, зависимые классы.

Pimple предоставляет метод extend(), который помогает в подобных ситуациях. Посмотрите на следующий пример:

Теперь мы можем использовать новую версию сервиса tweet_service. Первый аргумент метода extend() — это имя существующего сервиса, а второй — это замыкание, которое получает экземпляр сервиса и контейнера Pimple.

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

Итоги

Управление зависимостями является одним из краеугольных камней при разработке веб-приложений. Мы можем внедрять зависимости в конструкторах классов или использовать для этого специальные setter-методы, хотя это и тянет за собой свои недостатки. Pimple позволяет в большей степени избавиться от многих неудобств, давая возможность создавать приложения, следуя принципам DRY.

Источник: SitePoint




Внедрение зависимостей при помощи Pimple: 9 комментариев

      1. Обязательно, не узнал блог после редизайна, в rss статья прилетела.

        Было бы хорошо добавить твитер, если он есть. И линки на новые статьи туда постить тоже.

  1. Спасибо за статью. Как раз то, что нужно для необъемных приложений. И, на мой взгляд, полезно, то это легко применимо в фрейморках, в которых отсутствуют собственные классы, обеспечивающие создание и функциональность IoC контейнеров. Интересно, а на PHP создано что-нибудь из этой серии, но позволяющее конфигурировать работу этих контейнеров их XML-спецификации наподобие, как это делает Spring Framework на Java?

    1. Такого не встречал. Обычно наблюдается, что народ конфигурирует контейнеры через общие для фреймворка классы для работы с конфиг-файлами. Но не буду утверждать, опыта у меня маловато.

  2. Александр, здесь вот я заметил опечаточку в последнем примере с методом extend (). Там написано в аргументе лямбда-функции $twSservice, наверное нужно $twService? И еще вопрос по этому методу: наверное, тоже есть смысл поместить в контейнер реализацию (здесь гипотетического) класса ORM, и эти классы DB и ORM симплементировать с какого-то общего интерфейса, декларирующего, скажем, общий для обоих классов метод getQueryResults? Так, наверное, было бы удобнее для последующих юнит-тестов? Спасибо.

    1. Dmytro, спасибо за найденную опечатку, исправил. Что до вашей идеи, то вполне может быть, что это более лучшее решение. Однако статься переводная и поэтому осталась максимально такой, как она есть у автора ;)

Комментарии запрещены.