Phemto и Паттерн Dependency Injection. Часть 1

Original author: Marcus Baker
  • Translation
Я не встречал хорошего описания паттерна Dependency Injection применительно к PHP.

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

Я вспомнил еще об одной библиотеке для DI, Phemto. Ее автор, — Маркус Бэйкер, создатель SimpleTest. К сожалению на сайте содержится краткая и невнятная справка. тем не менее, проект развиавется, а внутри дистрибутива лежит статья с крайне хорошим объяснением про DI, ну и руководством конечно. Phemto, — очень миниатюрный проект, состоящий из трех не очень больших файлов.

Мне показалось, полезным перевести статью на русский язык и выложить сюда. Статья не очень большая, но содержательная. Ссылку на оригинал дать не могу, оригинал внутри дистрибутива :)


На программистском жаргоне, Phemto – это легкий, автоматизированный контейнер dependency injection (управления зависимостями). Проще говоря, задача Phemto – создавать экземпляр объекта, получая минимум информации, таким образом, значительно ослабляя зависимости внутри приложения или фреймворка.

Зачем это нужно?

Проще всего понять паттерн DI это представить себе шкалу с «Используем DI» на одном конце и «Используем хардкодинг (т.е. жестко запрограммированные связи)» на другом. Мы с вами сейчас устроим маленькое путешествие от хардкодинга через паттерны Factory, Registry, Service Locator к DI. Если Вы и так знаете, что такое DI, переходите сразу к
установке Phemto.

Заурядное создание объектов с помощью оператора new выглядит простым и понятным, но мы, скорее всего, столкнемся с трудностями, когда захотим что-то поменять потом. Посмотрим на код…


class MyController { 
    function __construct() { 
        ... 
        $connection = new MysqlConnection(); 
    } 
}


Здесь MyController зависит от MysqlConnection.

Оператор new ясен и понятен, но MyController сможет использовать только БД MySQL. Немного переделать класс, чтобы было можно его наследовать едва ли поможет, т.к. тогда мы будем иметь в наследнике вместе с логикой дочернего контроллера и логику получения драйвера БД. В любом случае множественные зависимости не решаются наследованием, приводя к захламлению класса. Вообще говоря, Вы можете разыграть карту наследования только однажды.

Следующий шаг, – используем Factory


class MyController { 
    function __construct($connection_pool) { 
        ... 
        $connection = $connection_pool->getConnection(); 
    } 
}


Очень эффективное решение. Фабрика может быть настроена на нужный тип драйвера с помощью конфигурационного файла или явно. Фабрики часто могут создавать объекты из разных семейств объектов, и тогда их называют Abstract Factory (Абстрактная Фаброика) или Repository (Репозиторий). Однако тут есть ограничения.

Фабрики приносят много дополнительного кода. Если надо тестировать классы с помощью mock-объектов, то придется имитировать не только сами, возвращаемые фабрикой объекты, но и саму фабрику. Получаете немного дополнительной суеты.

Да и в живом коде, если нужно вернуть объект, о котором автор фабрики не подумал, то придется наследовать или переписывать и саму фабрику, что для фреймворков может оказаться заметной проблемой.

Следующий ход в нашей борьбе с зависимостями, это вообще вынуть создание объекта Registry из основного объекта наружу…


class MyController { 
    function __construct($registry) { 
        ... 
        $connection = $registry->connection; 
    } 
} 
... 
$registry = new Registry(); 
$registry->connection = new MysqlConnection(); 
... 
$controller = new MyController($registry);


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

Кроме того, с помощью такого подхода мы не сможем использовать ленивое создание объектов (lazy loading). Неудача ждет нас, и если мы захотим, чтобы нам возвращался не один и тот же объект адаптера к БД, а разные объекты.

Жизнь сразу ухудшится, если в нашем примере будут еще зависимости, которые надо учесть. Т.е. если, например, для создания объекта-адаптера недостаточно сделать new, а нужно добавить в конструктор какой-то еще объект. В общем, предварительная настройка грозит сделаться весьма запутанной.

Мы можем сделать паттерн Registry более изощренным, если позволим объекту Registry самостоятельно создавать экземпляры нужных объектов. Наш объект стал Сервис-локатором (Service Locator)…


class MyController { 
    function __construct($services) { 
        ... 
        $connection = $services->connection; 
    } 
} 
... 
$services = new ServiceLocator(); 
$services->connection('MysqlConnection'); 
... 
$controller = new MyController($services);


Теперь настройки, могут быть в любом порядке, однако ServiceLocator должен знать, как создать MysqlConnection. Задача решается с помощью фабрик или с помощью трюков с рефлексией, хотя передача параметров, может стать весьма кропотливой работой. Жизненный цикл объектов (напр. возвращать один и тот же объект, или создавать разные) теперь под контролем программиста, который может как, запрограммировать все в методах фабрики, так и вынести все в настройки или плагины.

К сожалению, эта почти серебряная пуля имеет ту же проблему, что и Registry. Любой класс, который будет пользоваться таким интерфейсом, неизбежно будет зависеть от Сервис-локатора. Если Вы попробуете смешать две системы с разными сервис-локаторами, вы почувствуете что такое «не повезло».

Dependency Injection заходит немного с другой стороны. Посмотрим на наш самый первый пример…


class MyController { 
    function __construct() { 
        ... 
        $connection = new MysqlConnection(); 
    } 
}


… и сделаем зависимость внешней...

class MyController { 
    function __construct(Connection $connection) { 
        ... 
    } 
}


На первый взгляд, это просто ужасно. Теперь ведь каждый раз в скрипте придется все эти зависимости руками трогать. Изменить адаптер к БД придется вносить изменения в сотне мест. Так бы оно и было, если бы мы использовали new


$injector = new Phemto(); 
$controller = $injector->create('MyController');


Хотите верьте, хотите нет, но это все, что нам нужно.

Задача Phemto – выявление того, как создать объект, что позволяет на удивление здорово автоматизировать разработку. Только по типу параметра в интерфейсе он выведет, что MysqlConnection – единственный кандидат, удовлетворяющий нужному типу Connection.

Более сложные ситуации, могут потребовать дополнительной информации, которая обычно содержится в «цепочечном» файле. Вот пример такого файла из реальной жизни, чтобы можно было почувствовать мощь паттерна…


require_once('phemto/phemto.php'); 
 
$injector = new Phemto(); 
$injector->whenCreating('Page')->forVariable('session')->willUse(new Reused('Session')); 
$injector->whenCreating('Page')->forVariable('continuation')->willUse('Continuation'); 
$injector->whenCreating('Page')->forVariable('alerts')->willUse('Alert'); 
$injector->whenCreating('Page')->forVariable('accounts')->willUse('Accounts'); 
$injector->whenCreating('Page')->forVariable('mailer')->willUse('Mailer'); 
$injector->whenCreating('Page')->forVariable('clock')->willUse('Clock'); 
$injector->whenCreating('Page')->forVariable('request')->willUse('Request'); 
return $injector;


Такое количество настроек типично для проекта среднего размера.

Теперь контроллер задает только интерфейс, а работа по созданию объектов выполняется посредником.
MyController теперь не должен вообще знать про MysqlConnection.
Зато $injector знает и о том и о другом. Это называется обращение контроля Inversion of Control.


Продолжение в части 2
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 31

    +1
    Ну в редакторе же есть тэг, code. А за перевод, спасибо!
      0
      он не подсвечивает, к сожалению.
        +1
        кажется мне подсказали, как сделать. сейчас займусь.
        +1
        highlight.hohli.com/ — вот здесь например, поставьте галочку для Хабрахабра и смело вставляйте то, что он сгенерит
          0
          спасибо
            +1
            Эти велосипеды ломают подсветку в кастомных темах, например. А всего навсего надо было использовать тег <source>

            require_once('phemto/phemto.php'); 
             
            $injector = new Phemto(); 
            $injector->whenCreating('Page')->forVariable('session')->willUse(new Reused('Session')); 
            $injector->whenCreating('Page')->forVariable('continuation')->willUse('Continuation'); 
            $injector->whenCreating('Page')->forVariable('alerts')->willUse('Alert'); 
            $injector->whenCreating('Page')->forVariable('accounts')->willUse('Accounts'); 
            $injector->whenCreating('Page')->forVariable('mailer')->willUse('Mailer'); 
            $injector->whenCreating('Page')->forVariable('clock')->willUse('Clock'); 
            $injector->whenCreating('Page')->forVariable('request')->willUse('Request'); 
            return $injector;
            


            P.S. сорри за АП, статью привели как пример поломанной подсветки.
          0
          Хорошая статья, спасибо!
            –1
            Минус. Причем самому паттерну.

            Потому-что в процессе работы люди используют автозавершение по типам.

            Именно поэтому в итоге на C# удобнее работать, нежели на нативном PHP.

            Это я со временем понял и теперь на PHP все время использую только какой-нить Ecplise с завершением по типам.

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

            Либо автор неудачные примеры привел.
              0
              Пожалуйста ответьте более развернуто.

              Что такое завершение по типам, и как оно решает ту задачу, что описана в этой статье?

              Как именно помогает Eclipse справиться с зависимостями между классами?
                –1
                1. Завершение — это когда мы к примеру в сишарпе создаем объект:
                Human petya = new Human();
                // Потом пишем
                petya.
                // и нажимаем Ctrl + Space
                // IDE выдает нам все методы и свойства класса Human

                Аналогично в eclipse и прочих IDE.

                2. Eclipse никак не помогает справится с зависимостями.
                Я вообще не увидел здесь никакой проблемы.
                Пожалуйста проблему почетче опишите.
                  0
                  Посмотрите пожалуйста примеры использования во второй части статьи, особенно вот этот:

                  — Пусть мы хотим написать реализацию Authentication на основе интерфейса фреймворка…
                  Наш компонент будет использовать общее с фреймворком подключение к БД, и еще мы хотим взять кэширующий компонент третьей стороны.
                  — на мой взгляд это потенциально трудное место весьма элегантно программируется с помощью этого паттерна.

                  я, в свою очередь, совсем не понял к чему тут автозавершение, которое дает IDE? Где паттерн мешает работе с методами и интерфейсами?
                    +1
                    = По завершению =

                    $controller = $injector->create('MyController');

                    В ПХП это позволительно. Но потом каким образом указать IDE, что тип $controller — MyController?

                    Обычно это указывается в самом методе create, как @return {MyController}.

                    В СиШарпах и проч. можно сделать так:
                    MyController controller = ((MyController)injector->create('MyController'));

                    И далее работать с объектом.

                    В Eclipse по-мойму можно сделтаь так:
                    /**
                    * @type {MyController}
                    */
                    $controller = $injector->create('MyController');

                    Но это опять же гемор, только сбоку.

                    Вторую часть читаю.
                      0
                      Про IDE понял. Мда. Но с другой стороны это получается ради IDE корячиться с кодом. Ну блин, а как же с полиморфизмом, т.е. возвращает тебе фабрика разные типы объектов с одинаковым интерфейсом? Ну наверное для жавы и сишарп IDE умеет это отследить… угу. Для PHP такого пока нет.

                      Хорошо, что я на это не подсел, хихик, хотя признаю, что удобно.
                        0
                        :) Для ПХП такое тоже по-мойму возможно:

                        /**
                        * @type {MyController}
                        */
                        $controller = $injector->create('MyController');

                        С кодом не надо корячиться — надо сначала моделировать, а потом его генерить.

                        Всё.

                        Зато потом, когда проект начинает быть «пипец» каким большим, то весьма это помогает.

                        Исптытайте, тык скыть эффект масштаба.

                        А подсесть на это Вам придется со временем.

                        Напр., когда выйдет нормальная IDE, в которой это всё есть сразу, а не надо корячиться и собирать её.
                          +1
                          >С кодом не надо корячиться — надо сначала моделировать, а потом его генерить.

                          оно конечно здорово, но код живет и развивается, все заранее не спроектируешь. Если бы так было, как Вы говорите, то рефакторинг нужен бы не был.
                            0
                            отвечает пользователь Eclipse(plug. PHPE)
                            @return datatype description
                              0
                              Неа, не получится.

                              Из того метода мы не знаем заранее, что вылезет.

                              Так что именно так для нормальных методов делают.

                              А здесь — ненормальный :)
                                0
                                Вообще, все проще:

                                /* @var $controller MyController */
                                $injector = new Phemto();
                                $controller = $injector->create('MyController');

                                Таким образом любая IDE (Zend, Zend for Eclipse точно) подцепят автодополнение и т.п.
                          0
                          код все-таки первичен ИМХО.
                            0
                            :) первична задача, которая оплачивается заказчиком.

                            А код — это всего лишь «один из».

                            И вообще по мне первичен не код, а его качество.

                            Это как бэ смещение акцентов:
                            «Деньги — не главное. Главное их количиство».

                            Так что работайте в этом направлении ещё усиленней. Либо набирайтесь опыта. Кароче это как бэ дзен. Оно пришло ко мне с неба :)
                              0
                              так я окачестве и говорю — не жертвовать же качеством ради IDE.

                              ну спасибо за дзен конечно.

                              просто как бы в статье дзен, но записанный в виде кода. A у Вас он какой-то непостижимый моим умом пока… :) Но дзен он да… он приходит.
                        0
                        вторая часть тут:
                        habrahabr.ru/blogs/php/64078/
                          0
                          Ничто не мешает сделать сервисный класс, знающий и отдающий нужные типы, а внутри использующий SC, IOC.
                            0
                            Как бэ вернулись к тому же, отчего якобы уходим?

                            Вообще этот подход уже начинает больше склоняться к аспект-ориентированному подходу, нежели, к объектно-ориентированному.

                            Причем я всё еще не улавливаю сути его?

                            Это типа для тех иррационалов, которые сначала делают, а потом думают???

                            Цель!!! Для каждого движения нужна цель.

                            Здесь я понимаю цель такая, чтобы вначале делать, а потом думать.
                        0
                        Только сейчас наткнулся на статью, так что откомменчу через 1.5 года только :-)

                        1. Для .net точно так же есть ninject/castle windsor.
                        2. Представьте себе задачу: у вас есть приложение, которое по каким-то событиям отправляет почту.

                        Очевидно, что во время отладки/разработки по настоящим живым адресам рассылать ничего не надо.

                        Как я решил такую проблему с ninject: в зависимости от окружения (dev/stage/prod) у меня инжектятся разные классы для уведомлений: SmtpTransport или FileTransport.

                        Таким образом единственное место, которое мне пришлось изменить для решения — это код, конфигурирующий контейнер.

                        Каким будет ваше решение?
                        0
                        Вот кстати ещё две неплохие статьи на эту тематику

                          +2
                          wiki.agiledev.ru/doku.php?id=ooad:dependency_injection

                          wiki.agiledev.ru/doku.php?id=ooad:manage_dependencies_in_php_code

                          Что-то с хабром у меня сегодня не лады :(
                            +1
                            Ага, я знаю, хорошие статьи.

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

                            Потом можно уже дополнительное почитать.

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

                            Вот еще одна презентация, которая обладает таким свойством, но опять же на англ.:
                            Презентация Джеффа Мура на PHP-tek, 2007
                            0
                            >Ссылку на оригинал дать не могу, оригинал внутри дистрибутива :)
                            Вспомнил как то __autoload вынесли в класс потом хотели его загрузить ))
                              0
                              Если честно, особой пользы для себя не увидел. Хотя не удивлюсь, если просто не сталкивался с подобной необходимостью или просто использовал другой подход.
                              • UFO just landed and posted this here

                                Only users with full accounts can post comments. Log in, please.