В этом документе…

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 довольно дорогие машинки.

Ссылки и дополнительная информация