Phemto за 10 минут
В этом документе…
- Что такое Dependency Injection?
- Установка из тарболла.
- Phemto в Вашей программе.
- Wiring syntax ("цепочный" синтаксис).
Dependency Injection за 5 минут
На программистском жаргоне, Phemto – это легкий, автоматизированный контейнер dependency injection (управления зависимостями). Иначе говоря, задача Phemto – создавать экземпляр объекта, получая минимум информации, значительно ослабляя зависимости внутри приложения или фреймворка.
Зачем это нужно?
Проще всего понять паттерн DI можно, представив себе шкалу с "Используем DI" на одном конце и "Используем хардкодинг (т.е. жестко запрограммированные связи)" на другом. Мы с вами сейчас проделаем короткое путешествие от хардкодинга через паттерны Factory, Registry, Service Locator к DI. Если Вы и так знаете, что такое DI, переходите сразу к установке Phemto.
Заурядное создание объектов с помощью оператора new выглядит простым и понятным, однако скорее всего, мы столкнемся с трудностями, когда захотим что-то поменять потом. Посмотрим на код...
<?php class MyController { function __construct() { ... $connection = new MysqlConnection(); } }
Здесь MyController зависит от MysqlConnection.
Оператор new ясен и понятен, но MyController сможет использовать только БД MySQL. Небольшая переделка класса, позволяющая его наследовать, не спасет, поскольку тогда мы будем иметь в наследнике вместе с логикой дочернего контроллера и логику получения драйвера БД. В любом случае множественные зависимости не решаются наследованием, приводя к захламлению класса. Вообще говоря, Вы можете разыграть карту наследования только однажды.
Следующий шаг, – используем Factory...
<?php class MyController { function __construct($connection_pool) { ... $connection = $connection_pool->getConnection(); } }
Очень эффективное решение. Фабрика может быть настроена на нужный тип драйвера с помощью конфигурационного файла или явно. Фабрики могут создавать и объекты из разных групп, и тогда их называют Abstract Factory (Абстрактная Фабрика) или Repository (Репозиторий). Однако тут есть ограничения.
Фабрики приносят много дополнительного кода. Если надо тестировать классы с помощью mock-объектов, то придется имитировать не только возвращаемые фабрикой объекты, но и саму фабрику. Получаете дополнительную суету.
Да и в живом коде, если нужно вернуть объект, о котором автор фабрики не подумал, то придется наследовать или переписывать и саму фабрику, что для фреймворков может оказаться заметной проблемой.
Следующий ход в нашей борьбе с зависимостями, это вообще вынуть создание объекта Registry из основного объекта наружу...
<?php 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)...
<?php class MyController { function __construct($services) { ... $connection = $services->connection; } } ... $services = new ServiceLocator(); $services->connection('MysqlConnection'); ... $controller = new MyController($services);
Теперь настройки, могут быть в любом порядке, однако ServiceLocator должен знать, как создать MysqlConnection. Задача решается с помощью фабрик или с помощью трюков с рефлексией, хотя передача параметров, может стать весьма кропотливой работой. Жизненный цикл объектов (напр. возвращать один и тот же объект, или создавать разные) теперь под контролем программиста, который может как, запрограммировать все в методах фабрики, так и вынести все в настройки или плагины.
К сожалению, эта почти серебряная пуля имеет ту же проблему, что и Registry. Любой класс, который будет пользоваться таким интерфейсом, неизбежно будет зависеть от Сервис-локатора. Если Вы попробуете смешать две системы с разными сервис-локаторами, вы почувствуете что такое "не повезло".
Dependency Injection заходит немного с другой стороны. Посмотрим на наш самый первый пример...
<?php class MysqlConnection { ... } class MyController { function __construct() { ... $connection = new MysqlConnection(); } }
...и сделаем зависимость внешней...
<?php class MysqlConnection { ... } class MyController { function __construct(Connection $connection) { ... } }
На первый взгляд, это просто ужасно. Теперь ведь каждый раз в скрипте придется все эти зависимости руками трогать. Чтобы изменить адаптер к БД, придется вносить изменения в сотне мест. Так бы оно и было, если бы мы использовали new...
<?php $injector = new Phemto(); $controller = $injector->create('MyController');
Хотите верьте, хотите нет, но это все, что нам нужно.
Задача Phemto – выявление того, как создать объект, что позволяет на удивление здорово автоматизировать разработку. Только по типу параметра в интерфейсе он выведет, что MysqlConnection – единственный кандидат, удовлетворяющий нужному типу Connection.
Более сложные ситуации, могут потребовать дополнительной информации, которая обычно содержится в "цепочечном" файле. Вот пример такого файла из реальной жизни, чтобы можно было почувствовать мощь паттерна...
<?php 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.
Установка Phemto
Phemto распростаняется простым тарболлом, так, что просто распакуйте его...
tar -zxf phemto_0.1_alpha6.tar.gz
Достаточно использовать require_once(), чтобы включить файл phemto.php
Единственная зависимость Phemto, это механизм PHP reflection.
Phemto в Вашей программе
Phemto лучше всего использовать в главном скрипте или главном классе приложения или фреймворка.
Сначала вы пишете классы, как обычно...
<?php class Permissions { ... } class Authentication { function __construct($permissions) { ... } } class MyPage implements Page { function __construct($authentication) { ... } }
Обычная архитектура Page controller. Мы можем легко сделать модульный тест для Page, поскольку его зависимость от Authentication передается в конструктор. Мы можем использовать версию-имитацию для теста и сконцентрироваться на логике.
Теперь мы напишем файл с цепочечной конфигурацией, назовем его "wiring.php". В нем содержатся все необходимые настройки для нашего приложения...
<?php require_once('phemto/phemto.php'); $injector = new Phemto(); $injector->forVariable('authentication')->willUse('Authentication'); $injector->whenCreating('Authentication') ->forVariable('permissions')->willUse(new Sessionable('Permissions')); return $injector; ?>
Здесь мы говорим нашему инжектору, что если он увидит аргумент $authentication, то нужно создать экземпляр Authentication. Объект для аргумента $permissions имеет другой жизненный цикл. Sessionable говорит о том, что если возможно, надо взять объект из сессии, иначе создать его и сохранить в сессии, так, что объект будет создан лишь однажды.
Наш главный скрипт вместо new теперь использует вызовы фабрик Phemto...
<?php require_once('lib/my_page.php'); $injector = include('wiring.php'); $page = $injector->create('Page'); ?> <html>...</html>
Таким образом, наш код настолько изолирован от главного скрипта, что мы можем добавлять и убирать зависимости между классами, безо всякого вмешательства в главный скрипт.
У авторов фреймворков обычно другая цель. Скорее всего у них уже есть центральная точка для создания страниц, и главная работа, это адаптировать компоненты сторонних разработчиков.
Пусть мы хотим написать реализацию Authentication на основе интерфейса фреймворка...
<?php interface Authentication { ... } class InternalFrontControllerActionChainThingy { function __construct(Authentication $authentication, ...) { ... } }
Наш компонент будет использовать общее с фреймворком подключение к БД, и еще мы хотим взять кэширующий компонент третьей стороны.
<?php require_once('cache.php'); class OurAuthentication implements Authentication { function __construct(Database $database, DatabaseCache $cache) { ... } }
Для фреймворка, основанного на фабриках, такой расклад близок к кошмару, поскольку фреймворк не знает, как создать компонент кэша, и куда его деть. Заставить нас передать фреймворку и фабрику, – не выход, поскольку фреймворк все равно должен будет, куда-то выдать и положить кэширующий компонент. Если же фреймворк использует Dependency Injection, то задача сводится всего лишь к настройке цепочки.
Цепочка может быть изменена напрямую с помощью пользовательского файла...
<?php $injector = include('framework/wiring.php'); $injector->willUse('OurAuthenticator'); return $injector; ?>
Однако, скорее всего, фреймворк поместит инструмент DI в свою систему регистрации...
<?php class FrameworkRegistration { ... static function register($class, $dependencies = array()) { $this->injector->whenCreating('Controller')->willUse($class); foreach (dependencies as $dependency) { $this->injector->whenCreating('Controller') ->whenCreating($class) ->willUse($dependency); } } }
И тогда мы можем сделать такой вызов...
<?php FrameworkRegistration::register('OurAuthentication', array('DatabaseCache'));
Цепочечный синтаксис Phemto
Простейший случай создания Phemto объекта, это через имя класса...
<?php class Porsche911 { } $injector = new Phemto(); $car = $injector->create('Porsche911');
Среди зарегистрированных классов будет найден подходящий.
Если только один класс может удовлетворить условию, тогда именно этого класса и будет создан объект. Phemto в этом вопросе достаточно умен и понимает абстрактные классы и интерфейсы...
<?php abstract class Car { } class Porsche911 extends Car { } $injector = new Phemto(); $car = $injector->create('Car');
Здесь $car – экземпляр класса Porsche911. Также и...
<?php interface Transport { } class Porsche911 implements Transport { } $injector = new Phemto(); $car = $injector->create('Transport');
Опять будет создан объект класса Porsche911, как единственно возможный вариант.
Если имеет место неясность, то Phemto бросит исключение. Неясность можно разрешить добавив в цепочку дополнительную информацию...
<?php interface Transport { } class Porsche911 implements Transport { } class RouteMaster implements Transport { } $injector = new Phemto(); $injector->willUse('Porsche911'); $car = $injector->create('Transport');
Это удобно и когда надо перекрыть стандартную реализацию, в то время как стандартный класс зарегистрирован в системе.
У Phemto есть два метода автоматического создания параметров. Первый, это с помощью типа...
<?php interface Engine { } class Porsche911 { function __construct(Engine $engine) { } } class Flat6 implements Engine { } $injector = new Phemto(); $car = $injector->create('Porsche911');
Это равнозначно new Porsche911(new Flat6()). Такой способ удобен авторам фреймворков, которым достаточно задать лишь имена интерфейсов.
Обратите внимание, – нам не пришлось менять основной код, даже несмотря на то, что мы поменяли сигнатуру конструктора.
Другой способ, – Phemto может создать параметр по имени аргумента...
<?php class Porsche911 { function __construct($engine) { } } interface Engine { } class Flat6 implements Engine { } $injector = new Phemto(); $injector->forVariable('engine')->willUse('Engine'); $car = $injector->create('Porsche911');
Опять мы для $car создаем объект класса new Porsche911(new Flat6()). Здесь мы воспользовались именем аргумента $engine, чтобы вычислить интерфейс. И дальше Phemto смог применить свои правила автоматизации.
Иногда все же надо передать параметры конструктору. Простейший способ сделать это, – добавить их в метод create...
<?php class Porsche911 { function __construct($fluffy_dice, $nodding_dog) { } } $injector = new Phemto(); $car = $injector->create('Porsche911', true, false);
Эти параметры займут свои места в конструкторе, в данном случае получится new Porsche911(true, false).
Неименованные параметры могут быть причиной ошибок, когда код станет посложнее, так, что можно воспользоваться и параметрами с именами...
<?php class Porsche911 { function __construct($fluffy_dice, $nodding_dog) { } } $injector = new Phemto(); $car = $injector->fill('fluffy_dice', 'nodding_dog') ->with(true, false) ->create('Porsche911', true);
Эти параметры также можно использовать и с зависимостями.
Phemto может вызывать и методы отличные от конструктора...
<?php interface Seat { } interface SportsCar { } class Porsche911 implements SportsCar { function fitDriversSeat(Seat $seat) { } } class BucketSeat implements Seat { } $injector = new Phemto(); $injector->forType('SportsCar')->call('fitDriversSeat'); $car = $injector->create('Porsche911');
Этот код аналогичен...
<?php $car = new Porsche911(); $car->fitDriversSeat(new BucketSeat());
Такой вызов методов, отличных от конструктора называется setter injection.
Далеко не всегда нужно создавать один и тот же объект. Иногда выбор должен определяться контекстом...
<?php interface Seat { } class Car { function __construct(Seat $seat) { } } class FordEscort extends Car; class Porsche911 extends Car; class StandardSeat implements Seat { } class BucketSeat implements Seat { } $injector = new Phemto(); $injector->willUse('StandardSeat'); $injector->whenCreating('Porsche911')->willUse('BucketSeat'); $car = $injector->create('Porsche911');
Можете быть уверены, – по умолчанию $seat будет объектом класса StandardSeat, но для Porsche911 будет использован BucketSeat.
Метод whenCreating() создаст новую вложенную версию Phemto, так, что в этом контексте можно употреблять все вышеупомянутые методы, т.е...
<?php class Car { function __construct($seat) { } } class FordEscort extends Car; class Porsche911 extends Car; class StandardSeat { } class BucketSeat { } $injector = new Phemto(); $injector->willUse('StandardSeat'); $injector->whenCreating('Porsche911') ->forVariable('seat')->willUse('BucketSeat'); $car = $injector->create('Porsche911');
Жизненный цикл объектов, созданных с помощью Phemto можно контролировать.
Phemto имеет встроенные классы: Factory (по умолчанию), который всегда создает новый экземпляр объекта, Reused который отдает ссылки на один и тот же экземпляр, и Sessionable, который хранит экземпляр объекта в системной переменной PHP $_SESSION. Они все наследуют от базового абстрактного класса Lifecycle. Разработчики могут расширять эти классы..
Здесь мы создадим единственный экземпляр объекта Porsche911 и будем раздавать ссылки
<?php class Porsche911 { } $injector = new Phemto(); $injector->willUse(new Reused('Porsche911')); $car = $injector->create('Porsche911'); $same_car = $injector->create('Porsche911');
$car и $same_car будут ссылаться на один и тот же объект. В конце концов, Porsche довольно дорогие машинки.
Ссылки и дополнительная информация
- Страница Phemto на SourceForge.
- Статья Мартина Фаулера description of DI.
- Архив с кодом. phemto_0.1_alpha10.tar.gz