Pattern: ServiceLocator

Статья Pattern: Registry имеет непосредственно отношение к этой статье, советую ознакомиться перед прочтением.

Паттерн ServiceLocator изначально спроектирован для Java и дает возможность находить сервисы, зная их имя. В Java для того чтоб найти Enterprise JavaBeans (EJB) или Java Message Service (JMS) нужно использовать JNDI API. Это накладывает на код некоторые ограничения, например операции поиска и создания компонентов служб могут быть сложными (ресурсоемкими) и могут использоваться периодически в различных клиентах приложения. Для этого ввели паттерн, который позволяет абстрагироваться от использования JNDI и скрытия сложностей создания исходного контекста, а также может повторно использоваться несколькими клиентами.

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

На картинке изображена диаграмма последовательности действий, показывающая взаимодействия между различными участниками ServiceLocator

Диаграмма взаимодействия между участниками ServiceLocator

Итак задача получить объект страницы для дальнейшего использования, зная только название объекта.

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


class ServiceLocatorExeption extends  Exception{
}

class InitialContextException extends  Exception{
}

class PageFactoryExeption extends  Exception{
}

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


class WhitePage{

}

Как понятно из названия искать мы будем объект страницы, возможно чтобы ассоциировать с ним какой-то контент, а возможно для того чтобы передать его шаблонизатору для отображения.

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


class PageFactory{
	public $initialContext = null;
	
	public function __construct($initialContext){
		$this->initialContext = $initialContext;
	}
	
	public function & factory(){
		try{
			// do somethind depends on $initialContext
			$product =  new WhitePage();
		}catch (Exception $e){
			throw new PageFactoryExeption();
		}
		
		return $product;
	}
}

Единственный метод (кроме конструктора) этой фабрики основываясь на пришедшем контексте должен выбрать какой объект он будет создавать, это может быть страница с текстом страница с уведомлением об ошибке, список страниц, или список списков. на данный момент это не важно поскольку у нас есть только «белая страница» с которой можно будет сделать все что угодно, даже распечатать и повесить в туалете.

Соответственно если создание не удалось, то бросается исключение, которое потом будет проброшено в сам ServiceLocator.

Идем дальше по цепочке. Следующий у нас объект InitialContext, который при создании должен получать в себя все доступные переменные окружения, и тоже имеет один метод (естественно кроме конструктора) который осуществляет поиск нужной фабрики и передаче ей переменных окружения, сама же фабрика возвращается в сам ServiceLocator.


class InitialContext{
	private $data;
	public function __construct(){
		 // do something depends on 
		 // $_GET, $_POST, $_COOKIE, $_FILES, $_SESSION, etc
		 if(isset($_SESSION))
		 	$this->data = $_SESSION;
		 else 
		 	$this->data = $_REQUEST;
	}
	
	public function & lookup($name){
		$factoryName = ucfirst($name)."Factory";
		try{
			$factory = new $factoryName($this->data);	
		}catch (Exception $e){
		/*
			try{
				$factoryName::getInstance();
			}catch(Exception $ee){
				throw new InitialContextException();
			}
		*/
			throw new InitialContextException();
		}
		return $factory;
	}
}

Тут очень хотелось в блоке catch сделать еще один try для попытки получения инстанса у синглтон объекта, но его пришлось закомментировать потому что PHP поддерживает конструкцию $factoryName::getInstance() только начиная с версии 5.3. В предыдущих версиях название класса должно быть указанно непосредственно. Разработчики объясняют это тем, что при начале синтаксического разбора все статические вызовы преобразуются в функции.

Ну и наконец то мы добрались до сладенького. То есть до самой реализации ServiceLocator. По наставлению Java класс реализовывает паттерн Singleton и имеет метод getService принимающий название сервиса и возвращающий сам сервис. Больше пока ничего нет, да ничего и не будет, наверное, потому что получать id объекта, а потом восстанавливать его по этому id неоткуда.


class ServiceLocator{
	private static $instance = null;
	private static $initialContext = null;

    final private function __construct(){
    	try {
    		$this->initialContext = new InitialContext();
    	}catch (Exception $e){
    		throw new ServiceLocatorExeption();	
    	}
    } 
	
	static function & getInstance(){ 
        if (!isset(self :: $instance)) 
            self :: $instance = new self; 
        return self :: $instance; 
    }
	
	public function & getService($name){
		if (empty($name))
			throw new ServiceLocatorExeption();
		
		try{
			$serviceFactory = $this->initialContext->lookup($name);
			$service = $serviceFactory->factory();
		}catch (InitialContextException $e){
			throw new ServiceLocatorExeption();
		}catch (PageFactoryExeption $e){
			throw new ServiceLocatorExeption();
		}

		return $service;	
	}
}

Ну я думаю тут тоже ничего сложного нет. Приватный конструктор при единственном вызове создает InitialContext и сохраняет его. getInstance обеспечивает доступ к инстансу, а getService запускает всю вышеописанную цепочку и возвращает нам нашу белую страничку. Для запуска все этого добра требуется всего две строчки:


$serviceLocator = ServiceLocator::getInstance();
var_dump($serviceLocator->getService("page"));
// object(WhitePage)#4 (0) { }

Вообще эта статья была написана в поисках того как можно объединить паттерн Registry и Pattern ServiceLocator. Вышло следующее, но реально применять на сайте я пока не стал из-за отсутствия наличия PHP 5.3 на сервере.



class PageFactory{
	public $initialContext = null;
	
	public function __construct(){
		 if(isset($_SESSION))
		 	$this->initialContext = $_SESSION;
		 else 
		 	$this->initialContext = $_REQUEST;
	}
	
	public function & factory(){
		try{
			// do something depends on $initialContext
			$product =  new WhitePage();
		}catch (Exception $e){
			throw new PageFactoryExeption();
		}
		
		return $product;
	}
}

Классом InitialContext я пожертвовал, решив, что он лишний в этой пищевой цепочке и переложил его функции частично на класс PageFactory и частично на ServiceLocator::factory. Новый ServiceLocator основан на всем, что было в паттерн Registry, по сути, добавляя только один новый метод getObjId. Метод getService является синонимом register с той лишь разницей, что добавляет уникальный идентификатор и дает хранить несколько объектов одного типа. Метод getObj это обертка для extract. А вот метод getObjId это уникальная плюшка класса ServiceLocator которая дает возможность получать id имея объект, операция противоположная для getObj. И, наконец переписан метод factory добавляя дополнительную возможность создания объекта через его фабрику. Например, если есть page но не известно как его создать сначала будет создана фабрика PageFactory, а потом будет попытка создать из нее что-нибудь, используя метод factory.


require_once("class.registry.php");

class ServiceLocator extends Registry {

	/**
	 * Looks up for services
	 *
	 * @param string $name
	 * @return mixed
	 */
	static public function & getService($name){
		$id = md5(time());
		$service = self::register($name,$id);
		return $service;
	}

	/**
	 * Restore object by id
	 *
	 * @param string $id
	 * @return mixed
	 */
	static public function & getObj($id){
		return self::extract($id);	
	}	
	
	/**
	 * extracts object id
	 *
	 * @param mixed $obj
	 * @return string
	 */
	static public function & getObjId($obj){
		$reg = self::__instance();
		// array_search($obj, $reg->tools) ???
		foreach ($reg->tools as $key => &$val)
			if($obj === $val)
				break;
		return $key;
	}
	
	/**
	 * Implementation of factory pattern
	 *
	 * @param string $name
	 * @param array $param
	 * @param string $func
	 * @return object
	 */
	static public function & factory($name,$params=null,$func="__instance"){
		
		if (is_object($name)) 
			return $name;
		
		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 Exception("Class '$name' doesn't declared and can't be loaded so does it's factory");

	}
}

Прежде чем вы попробовали запустить этот код, спешу заметить что работать код будет только на PHP 5.3, хотя сам я пока не проверял. По причине указанной выше статические методы классов наследников не перезаписывают статические методы классов родителей. Для того чтобы пример заработал нужно добавить новые методы в класс паттерн Registry и переписать метод factory.

2 Комментарии “Pattern: ServiceLocator

  1. Перед попыткой переноса того или иного функционала на другую платформу я бы сначала попробовал ответить на вопрос «А нафига?».

    Как следует из «Core J2EE Pattern Catalog» предназначение Service Locator заключается в «Service lookup and creation involves complex interfaces and network operations». Например, тебе нужно создать, отконфигурить и отдать клиенту… хотя бы выдавалку результатов поиска из гугла или из яндекса, или хитрый SAOP интерфейс, то можно смотреть в сторону Service Locator.

  2. ок допустим есть класс MyClass который надо создать
    $myObj = ServiceLocator::getService(«MyClass»,array(«params»));
    для этого будет вызвана MyClassFactory которая сконфигурирует объект и ты сможешь отдать его клиенту

Комментарии закрыты