Pull to refresh

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

Reading time 6 min
Views 24K
Original author: Marcus Baker
Я не встречал хорошего описания паттерна 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
Tags:
Hubs:
+35
Comments 31
Comments Comments 31

Articles