Цепочки объектов.

syfisher

TDD infected!!
Прошу, парни:

http://www.phpconf.ru/ptt/7_syfisher_dependency_management_phpconf2007.ppt

Текст доклада будет в wiki завтра.
 

Develar

Новичок
на основе прочитанного к Crazy просьба прокомментировать фразу "Мы не встречались с приложениями, где бы потребовались IoC контейнеры". То есть применяете ли вы на практике и для какой задачи пико/нано контейнеры?
 

Crazy

Developer
Я презентацию пока не смотрел :) но по поводу реплики отвечу: не бывает задач, которые требуют применения IOC. Боле того, не бывает задач, требующих применения ООП, TDD, структурного программирования и т.п.
 

Wicked

Новичок
Crazy
даже таких: "сделать мне то-то и то-то с использованием ООП, ТДД, ..." ? :)
 

Crazy

Developer
Wicked, это не задача. Это блажь.

-~{}~ 28.05.07 21:15:

...равно как "сделайте мне проект, чтобы в каждой строке кода число непробельных символов было кратно 7" :)
 

Crazy

Developer
whirlwind, я все равно не буду смотреть презентацию до публикации полного текста. :)
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Автор оригинала: Crazy
Если речь идет о паттерне IoC, то он упомянут исключительно как приятная фича контейнеров. Можешь забыть про это. Или -- на твой выбор -- я могу про это подробнее рассказать.
Спасибо за рассказ про наноконтейнеры! Это дало мне просветление по теме, над которой я думал около месяца.
Если можно вернуться к рассказу про IoC - я буду рад прочитать :)
 

Crazy

Developer
Хорошая статья, 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;
  }
  
}
 

syfisher

TDD infected!!
Хе, весьма интересно. Забавно, вместо того, чтобы переносить знания о связях между классами в контейнер - ты вынес их в отдельные файлы. Так сказать, чистый php-way.

Твой вариант содержит и плюсы, и минусы:
Плюсы:
1) Очень простое решение
2) Легко конфигурируется
Минусы:
1) Много мелких файлов
2) Сложности в подмене (мне так показалось), так как завязка на файлы. Но здесь можно внедрить stub, так как resolver передается в контруктор.

Насчет того, что IoC контейнеры народ переносит с сильной оглядкой на Java и это не идет на пользу - на 100% согласен.
 

Crazy

Developer
Автор оригинала: syfisher
Хе, весьма интересно. Забавно, вместо того, чтобы переносить знания о связях между классами в контейнер - ты вынес их в отдельные файлы. Так сказать, чистый php-way.
Собственно, это ничем не отличается от выноса в XML. Но на PHP действительно стоит писать в PHP-стиле. :)

1) Много мелких файлов
Это не всегда -- минус. Не говоря уже о том, что хорошо видна история изменений "конфига" в svn.

2) Сложности в подмене (мне так показалось), так как завязка на файлы. Но здесь можно внедрить stub, так как resolver передается в контруктор.
Это как раз предельно просто. Возьмем тупой вариант для подмены создания Credentials:

Код:
class LocalCredentialsFilter {

  var $m_setupDir;
  var $m_nested;

  function LocalCredentialsFilter($setupDir, $nested = null) {
    $this->m_setupDir = $setupDir;
    $this->m_nested = $nested;
  }

  function & resolv ($name) {
    if ($name == 'Credentials') {
      return array('single', $this->m_setupDir.$name.'-local.inc');
    } else if ($this->m_nested != null) {
      return $this->m_nested->resolv($name);
    } else {
      return null;
    }
  }
   
}
соответственно:

Код:
$ioc = new IOC(new SetupPathResolver('setup/', new LocalCredentialsFilter('local/')));
...и создаем local/Credentials-local.inc:

Код:
<?php
require_once 'Credentials-class.inc';
$instance =& new Credentials('root', '12345');
?>
Получаем:

Код:
connection Object
(
    [m_credentials] => credentials Object
        (
            [m_name] => root
            [m_password] => 12345
        )

    [m_statements] => Array
        (
            [0] => delete from Foo
            [1] => delete from Bar
        )

)
Естественно, все это можно оформить гораздо изящнее. :) Например -- сделать так, чтобы ресолверы искали последовательно в experimental/setup и т.п.
 

syfisher

TDD infected!!
Автор оригинала: Crazy
Собственно, это ничем не отличается от выноса в XML. Но на PHP действительно стоит писать в PHP-стиле. :)
Отличается как раз тем, что можно использовать все средства языка, и не нужно учить новый набор правил для создания XML-файла.

Автор оригинала: Crazy
Это не всегда -- минус. Не говоря уже о том, что хорошо видна история изменений "конфига" в svn.
В целом, согласен.

А у тебя есть пакеты? Как ты объединяешь наборы файлов или же каждый раз все собирается для приложения в отдельную папку? (В принципе, можно, как ты указал, последовательно смотреть по папкам - по убыванию приоритета).

В общем - я себе эту тему в избранное кладу ;)

Модераторам: перенесите тему в раздел по теории, пожалуйста, чтобы не затерялась.
 

Crazy

Developer
В действительности этот код был написан специально для поста в тот тред. :) Соответственно, у меня нет опыта его использования.
 
Сверху