Xpoint
   [напомнить пароль]

Factory - как создавать объекты и их инициализировать в одном месте?

Метки: [без меток]
2008-05-08 15:19:07 [обр] Pro PHP[досье]

При написании сложной модульной системы приходится создавать объекты некоторых классов, входящих в состав библиотеки. Например, требуется получить объект базы данных из Zend_Db. При этом хочется чтобы мы вызвали метод и получили готовый для работы объект, например:

$objDb = $this->_objCreator->factory( 'Zend_Db' );

Все, мы получаем объект базы даных PDO_MYSQL, работаем с кодировкой UTF8. Это происходит потому что объект Creator "знает" настройки наших модулей и правильно их инициализирует, подставляя нужные параметры во время создания объекта и сразу после его создания (обращается к методам полученного объекта). Это очень удобно, особенно когда следует предоставить такой функционал различным разработчикам, которые хотят без особых сложностей получить готовый объект, не вникая в параметры этого объекта и так далее.

СУТЬ ВОПРОСА. Как вы видите создание подобного класса? Я сейчас создал такой класс, но он не удовлетворяет мои потребности, так как я чувствую что в нем что-то не так.

<?php
/**
 * Загрузчик модулей отвечает за создание объектов из библиотеки.
 *
 * Обязанности класса:
 * - создание объекта по его имени;
 * - инициализация объекта первоначальными настройками для обеспечения его работы.
 *
 * @package Kernel
 * @author Anton Danil'chenko <anton.danilchenko [a] gmail.com>
 */
class Kernel_Creator
{
   /**
    * Объект настроек сайта.
    * Содержит настройки для компонентов системы.
    *
    * @var Kernel_Config
    */
   private $_objSiteSettings = null;


   public function __construct( &$objSiteSettings )
   {
      $this->_objSiteSettings = $objSiteSettings;
   }


   /**
    * Возвращает ссылку на запрошенный объект библиотеки по его имени.
    *
    * @param string $strClassName полное имя класса.
    * @param string $mixConfigSectionName имя подсекции в файле конфигурации для данного класса ИЛИ массив настроек.
    * @return object
    */
   public function factory( $strClassName, $mixConfigSectionName='default' )
   {
      // проверяем сущетвование класса с заданным именем
      if ( !class_exists( $strClassName, true ) )
         throw new CreatorException( 'Нет класса с заданным именем: ' . $strClassName );
      // получаем настройки для создаваемого объекта
      if ( is_string( $mixConfigSectionName ) )
         $arrConfig = $this->_getObjectConfig( $strClassName, $mixConfigSectionName );
      else
         $arrConfig = $mixConfigSectionName;
      // создаем объект
      $objNew = $this->_getObjectLink( $strClassName, $arrConfig );
      // настраиваем объект для работы
      $this->_setObjectPrepare( $strClassName, $objNew );
      // возвращаем подгтовленный объект
      return $objNew;
   }


   private function _getObjectConfig( $strClassName, $strConfigSectionName )
   {
      $arrConfig = array();
      // создаем объект
      switch( $strClassName ) {
         case 'Zend_Db':
            $arrConfig = $this->_objSiteSettings->getValue( 'DATABASE', $strConfigSectionName );
            break;
      }
      // возвращаем массив настроек
      return $arrConfig;
   }


   private function _getObjectLink( $strClassName, $arrConfig )
   {
      $objNew = null;
      // создаем объект
      switch( $strClassName ) {
         case 'Zend_Db':
            // добавляем параметр работы с буфферизацией
            $arrConfig[ PDO::MYSQL_ATTR_USE_BUFFERED_QUERY ] = true;
            // делаем правильным тип адаптера БД
            $arrConfig['dbtype'] = 'PDO_' . strtoupper( $arrConfig['dbtype'] );
            // создаем объект Zend_Db
            $objNew = call_user_func_array(
                     array( $strClassName, 'factory' ),
                     array( $arrConfig['dbtype'], $arrConfig ) );
//            $objNew = Zend_Db::factory(
//                        'PDO_' . strtoupper( $arrConfig['dbtype'] ),
//                        $arrConfig );
            break;
         default:
            $objNew = new $strClassName( $arrConfig );
      }
      return $objNew;
   }


   private function _setObjectPrepare( $strClassName, &$objReal )
   {
      // создаем объект
      switch( $strClassName ) {
         case 'Zend_Db':
            // устанавливаем правильную кодировку выводимых символов
            $objReal->query( 'SET CHARACTER SET utf8' );
            $objReal->query( 'SET NAMES utf8' );
            break;
      }
   }
   
}

class CreatorException extends Exception
{
}
?>

Конструктор принимает объект, который содержит настройки (представьте что там массив значений). На основе этих настроек этот класс будет смотреть, какие настройки нужны тому иному создаваемому объекту, и при этом будет знать как создавать и чем инициализировать полученный объект. Затем готовый объект будет возвращен тому, кто его вызывает.

Обратите внимание на тот момент, что объект Creator создается ядром системы, и все модули системы получают на него ссылку для работы с его возможностями.

Жду ваших предложений по улучшению класса, чтобы его можно было использовать для создания объектов с несколькими параметрами. При этом некоторые объекты создаются через new, а некоторые через метод фабрики factory().

И интересует, как вы видите запись настроек в файле конфигурации сайта. Ведь эти настройки должны быть минимальными, чтобы файл конфигурации оставался компактным.

спустя 2 часа 17 минут [обр] Алексей Шоков(6/9)[досье]

А что Вас, собственно, не устраивает? Мне кажется, всё с вашим кодом нормально. Применить Zend_Config для работы с конфигурацией — так вообще всё прекрасно будет.

Жду ваших предложений по улучшению класса, чтобы его можно было использовать для создания объектов с несколькими параметрами. При этом некоторые объекты создаются через new, а некоторые через метод фабрики factory().

Уточните вопрос, пожалуйста. Вас интересует, как создать объекты с произвольным (заранее неизвестным) числом параметров? Если да, то используйте рефлексию.

спустя 2 дня 6 часов [обр] jmas[досье]
<?php

class Lib
{
   private static $instances = array();

   public function classes()
   {
      $args = func_get_args();

      foreach( $args as $class_name )
      {
         $class_file = self::getClassFile($class_name);

         $class_name = self::getClassName($class_name);

         self::loadClass( $class_file, $class_name );
      }
   }


   public static function loadClass( $class_file, $class_name = '' )
   {
      if( empty($class_name) )
      {
         $class_name = self::getClassName($class_name);
      }

      if( !class_exists( $class_name ) )
      {
         require_once( $class_file );
      }

      if( !class_exists($class_name) )
      {
         trigger_error( 'SC: Class "' . $class_name . '" not defined!', E_USER_ERROR );
      }
   }

   private static function getClassFile( $class_name )
   {
      if( substr($class_name, 0, 1) == '/' )
      {
         $class_file = ROOT . $class_name;

         $name_expl = explode('/', $class_name);

         $class_name = array_pop($name_expl);
      }
      else
      {
         $class_file = ROOT . '/lib/' . $class_name . '.class.php';
      }

      return $class_file;
   }

   private static function getClassName( $class_path_or_name )
   {
      if( substr($class_path_or_name, 0, 1) == '/' )
      {
         preg_match('/^.+\/(.+)\..+\.php$/', $class_path_or_name, $matches);

         $class_path_or_name = $matches[1];
      }

      return $class_path_or_name;
   }

   public static function getClass( $class_name )
   {
      $class_file = self::getClassFile($class_name);

      $class_name = self::getClassName($class_name);

      if( empty( self::$instances[$class_name] ) )
      {
         self::loadClass( $class_file, $class_name );

         //$class_name

         $args = func_get_args();

         $args_code = '';

         if( count($args) > 0 )
         {
            array_shift($args);

            for( $i=0; $i<count($args); $i++ )
            {
               $args_code .= '$args[' . $i . ']';

               if($i < (count($args)-1))
               {
                  $args_code .= ',';
               }
            }

            eval('$class = new ' . $class_name . '(' . $args_code . ');');
         }
         else
         {
            $class = new $class_name();
         }

            self::$instances[ $class_name ] = & $class;
      }

      return self::$instances[ $class_name ];
   }


   public static function replaceClass( $class_name, $new_class )
   {
      unset(self::$instances[$class_name]);

      self::$instances[$class_name] = $new_class;

      return self::$instances[$class_name];
   }


   public static function newObject( $class_name )
   {
      $class_file = self::getClassFile($class_name);

      $class_name = self::getClassName($class_name);

      self::loadClass( $class_file, $class_name );

      $args = func_get_args();

      $args_code = '';

      if( count($args) > 0 )
      {
         array_shift($args);

         for( $i=0; $i<count($args); $i++ )
         {
            $args_code .= '$args[' . $i . ']';

            if($i < (count($args)-1))
            {
               $args_code .= ',';
            }
         }

         eval('$class = new ' . $class_name . '(' . $args_code . ');');
      }
      else
      {
         $class = new $class_name();
      }

      return $class;
   }


   public static function isInit( $class_name )
   {
      return isset(self::$instances[$class_name]);
   }

}

?>

Вот мой класс для работы с библиотекой.

Создание нового объекта:
$Actions = Lib::getClass('Acrions');

С параметрами:
$Actions = Lib::getClass('Acrions', 'параметр1', 'параметр2');

Чтобы расширить возможности и инклюдить другие классы из других папок я сделал замароченную систему проверки путей... Не обращайте внимания. Главное сама идея :)

спустя 8 часов [обр] Алексей В. Иванов(509/2861)[досье]
спустя 3 дня [обр] Pro PHP[досье]

jmas[досье] спасибо за приведенный фрагмент, попробую понять что в нем да как, но чуть позже.

Я в общем то хочу решить следующую задачу - вызывать метод $objCreator->getObject( 'Zend_Db' ), и чтобы этот метод сам "знал" как нужно инициализировать запрошенный объект. Если такого объекта нет, то выкинуть исключение.

Поэтому я хочу понять, какие параметры стоит хранить в файле конфигурации (это может быть логин и пароль, а так же тип базы данных, которые должны быть скрыты от разработчика), а какие параметры сам разработчик может передавать в этот метод getObject() чтобы уточнить, какой именно объект ему нужно. Это поясняю ниже.

Пояснение необходимости уточнения объекта. Например, у вас есть две базы данных. Одна основная для сайта default, а другая - внешний сервер, где хранятся резервные копии БД (назовем ее reserve). Так вот, если вы запрашиваете так:

$objCreator->getObject( 'Zend_Db' );
// or
$objCreator->getObject( 'My_Favorite_Database' );

то получим в результате объект с DEFAULT настройками к локальной базе. Это прекрасно!

А вот если мы хотим получить объект для работы с резервным хранилищем, то запрос будет выглядеть так:

$objCreator->getObject( 'Zend_Db', array( 'schema'=>'reserve' ) );
// or
$objCreator->getObject( 'My_Favorite_Database', array( 'schema'=>'reserve' ) );

Все отлично. Именно для этого я и создаю данный класс. В чем же тогда вопрос? Мне нужно определиться с:

  1. как хранить настройки объекта, и метода его инициализации (вызов конструктора, или вызов метода ::factory);
  2. как гарантировать что объект для работы с базой reserve получит только модуль, которому разрешено его получать? Может быть, я перегибаю палку, но считаю что следует ограничить возможность получения объекта для работы с удаленной базой данных. Как это реализовать?
  3. следует определиться с тем, в каком формате будет передаваться второй параметр метода getObject(). Нужно ввести некоторый стандарт, или где-то описать, что такой-то объект может принимать такие то параметры.
  4. определиться, какие объекты вообще могут создаваться с дополнительными параметрами кроме базы даных. Возможно, введение второго параметра не имеет вод собой основания.

Вот такие задачи стоят. Я попытался максимально понятно выразить свои мысли. Надеюсь, вам тоже будет интересно реализовать подобный класс "Создатель/Творец" для своих нужд.

спустя 8 дней [обр] Pro PHP[досье]

Я снова пришел к этой задаче. Обдумал, вот как звучит задача: "следует создать класс, который будет создавать объекты указанного типа. При этом объект должен быть инициализирован начальными данными и подготовлен для работы. Часть настроек объекта хранится в файле конфигурации, а некоторые параметры нужно передавать вручную с места вызова $Creator->factory( ... )".

Поясняю вышесказанное. Есть глобальный объект $Creator, и мы к нему обращаемся с просьбой чтобы он дал нам "готовый для работы" объект (например типа Zend_Db). Класс Creator по типу объекта ищет его настройки (в массиве настроек), и создает объект с этими настройками.

Все хорошо, но есть одно НО - нам может понадобиться передавать некоторые "свои" параметры с места вызова. Во пример кода:

// получаем объект, но в параметре нужно предать другой объект (или несколько объектов)
$objNew = $Creator->factory( 'ReadMyRss', $objRssReader );
// ИЛИ получаем объект для работы с базой по имени схемы (настройки коннекта остаются скрытыми)
$objNew = $Creator->factory( 'Zend_Db', 'default' );
$objNew = $Creator->factory( 'Zend_Db', 'base1' );
$objNew = $Creator->factory( 'Zend_Db', 'base2' );
// ИЛИ комбинация вышесказанного
$objNew = $Creator->factory( 'Zend_Db', 'base2', array($objTest1, $objTest2) );
// ИЛИ мы указываем дополнительные настройки получаемого объекта (кроме тех, что берутся из массива конфигурации)
$objNew = $Creator->factory( 'Zend_Db', 'base2', array('username'=>'noRoot') );

Возможно, это я занялся фигней. Если это так, то скажите мне, в чем моя ошибка, и что мне может пригодиться, а что нет (исходя из вашей практики и опыта). Мне нужно чтобы меня "вывели" из бесконечного цикла. Станьте оператором Break для меня :-)

спустя 19 дней [обр] Sm0ke(14/19)[досье]
  1. как хранить настройки объекта, и метода его инициализации (вызов конструктора, или вызов метода ::factory);
Можно не хранить. Можно проверять, имплементирует ли тот класс, интервейс with_factory
Если да, то вызывать factory(), иначе new
interface with_factory
{
  static public function factory();
}

class my_base implements with_factory
{
  static public function factory()
  { return new self; }
}

$class= "my_base";
$obj= ($class instance_of with_factory)?call_user_func(array($class, 'factory')):new $class;
Powered by POEM™ Engine Copyright © 2002-2005