PHP

индекс
206,80

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

Я не встречал хорошего описания паттерна 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
+35
10 июля 2009, 14:29
71

комментарии (30)

+1
crazyprog #
Ну в редакторе же есть тэг, code. А за перевод, спасибо!
0
fantaseour #
он не подсвечивает, к сожалению.
+1
fantaseour #
кажется мне подсказали, как сделать. сейчас займусь.
+1
shuvalov #
highlight.hohli.com/ — вот здесь например, поставьте галочку для Хабрахабра и смело вставляйте то, что он сгенерит
0
fantaseour #
спасибо
0
ukko #
Хорошая статья, спасибо!
–1
garex #
Минус. Причем самому паттерну.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Всё.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вот еще одна презентация, которая обладает таким свойством, но опять же на англ.:
Презентация Джеффа Мура на PHP-tek, 2007
0
jandosul #
>Ссылку на оригинал дать не могу, оригинал внутри дистрибутива :)
Вспомнил как то __autoload вынесли в класс потом хотели его загрузить ))
0
iz0 #
Если честно, особой пользы для себя не увидел. Хотя не удивлюсь, если просто не сталкивался с подобной необходимостью или просто использовал другой подход.
0
EugeneOZ #
Много времени прошло с момента опубликования статьи, было интересно читать её, однако, есть в ней неточность:
Любой класс, который будет пользоваться таким интерфейсом, неизбежно будет зависеть от Сервис-локатора.

Неправда. В классах достаточно требовать не сам SL, а интерфейс, содержащий метод создания необходимого класса. Тогда нужно будет передать SL, который этот интерфейс реализует, и никакой проблемы с несколькими системами не возникнет, т.к. классы о локаторе знать не будут, о нём будет знать только точка инициализации.
Подробный пример здесь: DI Service Locator.

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