Хорошая статья, thanx. Разве что опечатку в фамилии госпожи Лисков стоит поправить.
И есть одно спорное место, на которое я хотел бы обратить внимание:
Глобальные переменные – самый простой способ и самый распространенный в ранних php-приложениях, да и сейчас такой способ часто используют. Этот способ можно в принципе отнести в pull-приему.
Я бы отнес этого как раз к push-приему. Поскольку переменная инициализируется ДО активации использующего ее кода и этот код не управляет ни выбором момента для инициализации, ни способом инициализации. Он получает ее как данность.
Далее -- об IOC-контейнерах. Меня в них привлекает прежде всего простота. К сожалению, java'овские реализации обросли кучей красивых, но сугубо опциональных фич, попытка переноса которых в PHP as is выглядит довольно неуклюже.
Начнем с того, что мы хотим выполнить некоторое действие и для этого нам нужен объект. И IOC-контейнер для нас -- всего лишь средство получить экемпляр этого объекта, не задумываясь о том, как этот объект будет создан и использует ли кто-то еще тот же экземпляр. Важно, что объект должен приходить к нам полностью сконфигурированный и готовый к использованию.
Поскольку здесь-и-сейчас у меня стоит PHP4, все примеры будут только на нем.
Пусть у нас есть класс Connection, который мы будем использовать для исполнения SQL-запросов. Используем вот такую заглушку:
Код:
class Connection {
var $m_credentials;
var $m_statements;
function Connection ($credentials) {
$this->m_credentials = $credentials;
$this->m_statements = array();
}
function post_configure() {
echo 'About to connect to database as '.$this->m_credentials->get_name().
'/'.$this->m_credentials->get_password();
}
function execute ($statement) {
$this->m_statements[] = $statement;
}
}
Для подключения к БД используется информация из класса Credentials:
Код:
class Credentials {
var $m_name;
var $m_password;
function Credentials($name, $password) {
$this->m_name = $name;
$this->m_password = $password;
}
function get_name() {
return $this->m_name;
}
function get_password() {
return $this->m_password;
}
}
Исходники положим в файлы Connection-class.inc и Credentials-class.inc соответственно.
Мы хотим, чтобы код, у которого возникла потребность исполнить запрос, не заморачивался проверкой того, как создать соединение, было ли оно создано раньше и где взять его параметры. Например, так:
Код:
$conn1 =& $ioc->get('Connection');
$conn1->execute('delete from Foo');
...
$conn2 =& $ioc->get('Connection');
$conn2->execute('delete from Bar');
Скорее всего, мы хотим, чтобы во втором фрагменте кода в целях повышения эффективности использовалось уже созданное соединение, но, как я уже отметил, это не должно влиять на вызывающий код.
Реализацию класс IOС, экземпляром которого является $ioc, начнем с метода get:
Код:
function & get($name) {
list($kind, $path) = $this->m_resolver->resolv($name);
if ($kind == 'single') {
if (isset($this->m_singleton[$name])) {
$instance =& $this->m_singleton[$name];
} else {
$instance =& $this->_instantiate($path);
$this->m_singleton[$name] =& $instance;
}
} else {
$instance =& $this->_instantiate($path);
}
return $instance;
}
Нам нужен объект, который превратит переданное вызывающим кодом имя объекта в пару тип-путь. Здесь тип указывает, нужно ли каждый раз создавать новый объект. Путь указывает, какой скрипт исполнять для создания нового объекта.
Метод _instantiate в нашем случае отвечает всего лишь за исполнение скрипта и выполнение дополнительной настройки уже созданного объекта:
Код:
function _instantiate($path) {
require $path;
if (method_exists($instance, 'post_configure')) {
$instance->post_configure();
}
return $instance;
}
Осталось понять: как по имени объекта узнать его множественность и путь к его скрипту. Эту работу мы возложим на класс SetupPathResolver. Обращаю внимание, что $filter здесь нужен прежде всего для тестирования и создания экспериментальных версий кода, позволяя выполнять любую нужную подмену.
Положим, что файл со скриптом должен имя ###-single.inc для синглетонов и ###-multiple.inc для объектов, которые создаются каждый раз заново. Код совершенно очевиден:
Код:
class SetupPathResolver {
var $m_setupDir;
var $m_filter;
function SetupPathResolver ($setupDir, $filter = null) {
$this->m_setupDir = $setupDir;
$this->m_filter = $filter;
}
function & resolv ($name) {
$pair = ($this->m_filter == null) ? null : $this->m_filter->resolv($name);
if ($pair == null) {
$pair = $this->_locate($name, 'single');
if ($pair == null) {
$pair = $this->_locate($name, 'multiple');
}
}
return $pair;
}
function _locate($name, $kind) {
$path = $this->m_setupDir.$name.'-'.$kind.'.inc';
if (file_exists($path)) {
return array($kind, $path);
} else {
return null;
}
}
}
Перейдем наконец к настройке наших объектов. Создадим в папке setup файл Connection-single.inc:
Код:
<?php
require_once 'Connection-class.inc';
$instance =& new Connection($this->get('Credentials'));
?>
Здесь мы имеем и ленивую загрузку кода класса Connection, и создание экземпляра.
Oops, нам нужен еще и объект Credentials. Создадим там же файл и для него: Credentials-multiple.inc. В данном случае мы разрешим создавать новый экземпляр на каждый вызов:
Код:
<?php
require_once 'Credentials-class.inc';
$instance =& new Credentials('Foo', 'Bar');
?>
В результате мы получаем такой прикладной код:
Код:
$ioc = new IOC(new SetupPathResolver('setup/'));
$conn1 =& $ioc->get('Connection');
$conn1->execute('delete from Foo');
...
$conn2 =& $ioc->get('Connection');
$conn2->execute('delete from Bar');
print_r($conn1);
Получаем:
Код:
connection Object
(
[m_credentials] => credentials Object
(
[m_name] => Foo
[m_password] => Bar
)
[m_statements] => Array
(
[0] => delete from Foo
[1] => delete from Bar
)
)
Тривиально. Просто (примерно 70 строк кода в движке).
P.S. Полный код класса IOC:
Код:
class IOC {
var $m_resolver;
var $m_singletons;
function IOC ($resolver) {
$this->m_resolver = $resolver;
$this->m_singletons = array();
}
function & get($name) {
list($kind, $path) = $this->m_resolver->resolv($name);
echo "[name=$name, kind=$kind, path=$path]";
if ($kind == 'single') {
if (isset($this->m_singleton[$name])) {
$instance =& $this->m_singleton[$name];
} else {
$instance =& $this->_instantiate($path);
$this->m_singleton[$name] =& $instance;
}
} else {
$instance =& $this->_instantiate($path);
}
return $instance;
}
function _instantiate($path) {
require $path;
if (method_exists($instance, 'post_configure')) {
$instance->post_configure();
}
return $instance;
}
}