Pattern: Factory

25 Июль 2007

Некоторые рассуждения о шаблоне проектирования Factory, а так же примеры реализации в "домашних" условиях наиболее универсального метода.

Шаблон Factory заключается в том, что метод одного объекта возвращает другой новый объект. Проиллюстрируем небольшим примером.


class Factory{
	public function factory(){
		return new Product();
	}
}

class Product{
}

$factory = new Factory();
$product = $factory->factory();

Хотя чаще фабричный метод делают статичным, но если метод нуждается в других ресурсах объекта то необходимо использовать еще и паттерн Singleton. Для этого создадим класс чуть по сложнее:


class Factory{
	private static $instance;
	public $name = 'Product';
	
	private function __construct(){
	}

	public static function __instance(){
		if (!isset(self::$instance)){
			self::$instance = new self;
		}
		return self::$instance;
	}

	public static function factory(){
		$instance = self::__instance();
		return new $instance->name();
	}
}

class Product{
}

$product = Factory::factory();

Теперь предположим что конструктор Product нуждается в параметрах, которые нужно передавать в фабричный метод.


	// чтобы не пложить код, я заменил один метод в прошлом примере
	public static function factory($a,$b,$c){
		$instance = self::__instance();
		return new $instance->name($a,$b,$c);
	}

Теоретически правильно, но параметров может быть неограниченное количество (я же тут за универсализм борюсь!).

Казалось бы есть выход сделать это при помощи call_user_func_array но таким образом можно вызывать только статичные методы, а конструктор не может быть статичным, поэтом делаем маленькую хитрость. При помощи встроеного класса ReflectionClass рефлекторно вызываем нужный нам класс с параметрами.


class Factory{
	private static $instance;
	public $name = 'Product';
	
	private function __construct(){
	}

	public static function __instance(){
		if (!isset(self::$instance)){
			self::$instance = new self;
		}
		return self::$instance;
	}

	public static function factory(){
		$instance = self::__instance();
		$args = func_get_args(); // func_get_args нельзя использовать в качестве агрумента другой функции.
		$reflection = new ReflectionClass($instance->name);
		return $reflection->newInstanceArgs($args); 
	}
}

class Product{
	public function __construct($a,$b,$c){
		echo $a,$b,$c;
	}
}

$product = Factory::factory('a','b','c');

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


class Factory {
	private static $instance;
	
	/**
	 * Constructor
	 *
	 */
	private function __construct(){
	}
	
	/**
	 * Singleton inplementation
	 *
	 * @return object
	 */
	public static function instance(){
		if (!isset(self::$instance))
			self::$instance = new self;
		return self::$instance;
	}
	
	/**
	 * Craft new object
	 *
	 * @param string $name
	 * @param array $params
	 * @param string $func
	 * @return object
	 */
	public static function factory($name,$params=null,$func='__instance'){
		if (!class_exists($name) && is_callable("__autoload"))
			__autoload($name);
		
		if (class_exists($name)){
			if(is_callable(array($name,$func))){
				return call_user_func_array(array($name,$func),$params); // метод $name::$func вызван статично
			}else if(!$params){ // пытаемся сэкономить время
				return new $name();
			}else{
				$reflection = new ReflectionClass($name);
				return $reflection->newInstanceArgs($params);
			}
		}else{
			$factory = ucfirst($name)."Factory";
			
			if (!class_exists($factory) && is_callable("__autoload"))
				__autoload($factory);

			if(class_exists($factory) && is_callable(array($factory,"factory"),true)){
				return call_user_func_array(array($factory,"factory"),$params);
			}
		}
		// если мы до сюда дошли и ничего не вернули то бросаем исключение
		throw new FactoryException("Class '$name' doesn't declared and can't be loaded so does it's factory");
	}
}

class FactoryException extends Exception{
}

class Product1{
	public function __construct(){
		echo "Product1 <br/>\n";
	}
}

class Product2{
	public function __construct($a,$b,$c){
		echo 'Product2 ',$a,$b,$c," <br/>\n";
	}
}

class Product3{
	private static $instance;
	
	private function __construct(){
		echo "Product3 <br/>\n";
	}

	public static function __instance(){
		if (!isset(self::$instance)){
			self::$instance = new self;
		}
		return self::$instance;
	}
}

class Product4{
	private static $instance;
	
	private function __construct($a,$b,$c){
		echo 'Product4 ',$a,$b,$c," <br/>\n";
	}

	public static function __instance($a,$b,$c){
		if (!isset(self::$instance)){
			self::$instance = new self($a,$b,$c);
		}
		return self::$instance;
	}
}

class Product51{
	public function __construct(){
		echo "Product51 <br/>\n";
	}
}

class Product52{
	private static $instance;
	
	private function __construct($a,$b,$c){
		echo 'Product52 ',$a,$b,$c," <br/>\n";
	}

	public static function __instance($a,$b,$c){
		if (!isset(self::$instance)){
			self::$instance = new self($a,$b,$c);
		}
		return self::$instance;
	}
}

class Product5Factory{
	public static function factory($a=null,$b=null,$c=null){
		if($a&&$b&&$c)
			return Product52::__instance($a,$b,$c);
		return new Product51();
	}
}


$product1 = Factory::factory('Product1');
$product2 = Factory::factory('Product2',array('a','b','c'));
$product3 = Factory::factory('Product3',null,'__instance');
$product4 = Factory::factory('Product4',array('a','b','c'),'__instance');
$product51 = Factory::factory('Product5');
$product52 = Factory::factory('Product5',array('a','b','c'));

Сразу оговорюсь что можно было бы сэкономить время вызвав $name::$func() (в строке 19) без параметров, если бы интерпритатор php перед тем как выполнить скрипт не преобразовывал вызовы всех статичных методов в вызовы функций, такая фича появиться с выходом PHP5.3

Для того чтобы создать один раз экземпляр класса Product этого кода, конечно очень много, но если постоянно приходиться создавать экземпляры классов имена которых динамичны, то это очень хороший подод.

  1. 4 Январь 2009 в 20:14 | #1
    Спасибо за подход с рефлексией! Буду применять :-)
  2. Вадим
    13 Июнь 2009 в 18:04 | #2
    Добрый вечер. Вы написали:
    
    	public static function factory(){
    		$instance = self::__instance();
    		$args = func_get_args(); // func_get_args нельзя использовать в качестве агрумента другой функции.
    		$reflection = new ReflectionClass($instance->name);
    		return $reflection->newInstanceArgs($args); 
    	}
    
    Скажите, а почему нельзя сделать так:
    
    	public static function factory(){
    		$instance = self::__instance();
    		$args = func_get_args(); // func_get_args нельзя использовать в качестве агрумента другой функции.
                    return new $instance->name($args);
    
    	}
    
  3. 13 Июнь 2009 в 18:34 | #3
    из-за аргументов, в первом варианте можно передать в конструктор несколько аргументов, как будто вызвали
    
    $instance->name($arg[0],$arg[1],...,$arg[n])
    
Комментирование отключено.