Proxy – пример использования в сервисном слое

Этим постом хочу открыть цикл заметок, который может быть полезен разработчикам, как начинающим, так и среднего уровня, желающим лучше разобраться в шаблонах проектирования и объектно-ориентированном дизайне программного обеспечения. В своё время я страдал от недостатка недостатка доступной русскоязычной литературы по теме, и двух крайностей в имеющихся публикациях: либо это сугубо академические, унылые диаграммы из которых не ясно когда и зачем данный шаблон применим, либо взятые с потолка, и не совместимые с жизнью примеры. Есть, конечно, “банда четырёх” и М.Фаулер (последнему отдельное огромное спасибо, как В.Кулакову, но реальных примеров, на мой взгляд, им недостаёт.

Если проще, то я покажу на конкретных примерах какие паттерны существуют, как их можно использовать, что это даёт и зачем нужно. Все примеры кода будут браться из “домашнего” проекта, которым я занимаюсь на досуге: phamily framework – это PHP фреймворк, для работы с родственными связями между людьми, именами, родословными и прочей генеалогией.

При написании фреймворка я использовал TDD-методологию, и, во многом благодаря ей, удалось добиться хорошего API, продуманной структуры классов и стабильности кода.

Итак, Proxy — Заместитель

Хватить болтать, пора глянуть код!

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

<?php
namespace phamily\framework\services;
 
use phamily\framework\models\Persona;
use phamily\framework\models\PersonaInterface;
use phamily\framework\value_objects\DateTimeInterface;
use phamily\framework\repositories\PersonaRepository;
use phamily\framework\repositories\PersonaRepositoryInterface;
use phamily\framework\services\proxies\PersonaRepositoryProxy;
 
class PersonaService implements PersonaServiceInterface{
 
	protected $repository;
 
	/*
	 * При создании сервиса создаётся Заместитель репозитория,
	 * по умолчанию  он не активен
	 */
	public function __construct(){
		$this->repository = new PersonaRepositoryProxy();
	}
	/*
	 * здесь происходит установка реального репозитория для заместителя
	 * в этот момент он становится активен
	 */
	public function useRepository(PersonaRepositoryInterface $repository){
		$this->repository->setRepository($repository);
	}
 
	/*
	 * этот метод даёт возможность в клиентском коде убедится
	 * был ли подключен репозиторий, то есть будут ли данные сохранены сервисом
	 */
	public function isRepositoryUsed(){
		return $this->repository->isActive();
	}
 
	/*
	 * создание новой персоны, которая будет при возможности сохранена в хранилище
	 */
	public function create(
			$gender = self::GENDER_UNDEFINED,
			array $names = [], 
			PersonaInterface $father = null,
			PersonaInterface $mother = null,			
			DateTimeInterface $dateOfBirth = null, 
			DateTimeInterface $dateOfDeath = null 
	){
		$persona = new Persona($gender, $names);
 
		$this->repository->save($persona);
 
		return $persona;
	}
 
	/*
	 * удаление персоны из памяти и
	 */
	public function delete(PersonaInterface &$persona){
		$this->repository->delete($persona);
 
		/*
		 * destroy object
		 */
		$persona = null;
	}
}

Ссылки на исходник в bitbucket.

Код сервиса приведён целиком, т.к. он ещё не очень большой, а каждый метод иллюстрирует использование репозитория. Репозиторий предоставляет интерфейс для записи, чтения и удаления доменного объекта из хранилища данных. Кстати вот он:

<?php
namespace phamily\framework\repositories;
 
use phamily\framework\models\PersonaInterface;
 
interface PersonaRepositoryInterface{
	public function save(PersonaInterface $persona);
	public function getById($id);
	public function delete(PersonaInterface $persona);
}

Ссылка на исходник в bitbucket.

А вот и сам наш Заместитель, заметьте он реализует тот же интерфейс:

<?php
namespace phamily\framework\services\proxies;
 
use phamily\framework\repositories\PersonaRepositoryInterface;
use phamily\framework\models\PersonaInterface;
 
class PersonaRepositoryProxy implements PersonaRepositoryInterface{
 
	protected $repository;
	protected $active = false;
 
	/*
	 * в момент создания есть возможность передать репозиторий,
	 * который будет использован
	 */
	public function __construct(PersonaRepositoryInterface $repository = null){
		if(isset($repository)){
			$this->setRepository($repository);
		}
	}
 
	/*
	 * возможность установить реальный репозиторий, который будет использован. 
	 * происходит активация Заместителя. 
	 */
	public function setRepository(PersonaRepositoryInterface $repository){
		$this->repository = $repository;
		$this->active = true;
	}
 
	/*
	 * а это способ проверить, есть ли реальный репозиторий, 
	 * перед тем как передавать ему вызовы. 
	 */
	public function isActive(){
		return $this->active;
	}
 
	public function save(PersonaInterface $persona){
		if($this->isActive()){
			return $this->repository->save($persona);
		}
	}
 
	public function delete(PersonaInterface $persona){
		if($this->isActive()){
			return $this->repository->delete($persona);
		}
	}
 
	public function getById($id){
		if($this->isActive()){
			return $this->repository->getById($id);
		}
	}
}


Ссылка на исходник в bitbucket.

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

Какие пользы сулит использование в сервисе такого подхода?

  1. Улучшенная возможность тестирования. Если бы реальный репозиторий создавался всегда вместе с сервисом, то для тестирования сервиса приходилось бы или конструировать реальное хранилище, либо заменять тестовой заглушкой. Оба вариант тянут накладные расходы и усложняют тест.
  2. Вынесение логики работы с хранилищем — она делегируется Заместителю. Перед рефакторингом, в процесс которого был выделен класс PersonaRepositoryProxy, код сервиса выглядел примерно так:

    <?php
    class PersonaService implements PersonaServiceInterface{
     
    	protected $repository;
     
    	public function useRepository(PersonaRepositoryInterface $repository){
    		$this->repository = $repository;
    	}
     
    	public function isRepositoryUsed(){
    		return isset($this->repository);
    	}
     
    	public function create(
    			$gender = self::GENDER_UNDEFINED,
    			array $names = [], 
    			PersonaInterface $father = null,
    			PersonaInterface $mother = null,			
    			DateTimeInterface $dateOfBirth = null, 
    			DateTimeInterface $dateOfDeath = null 
    	){
    		$persona = new Persona($gender, $names);
    		if($this->isRepositoryUsed()){
    			$this->repository->save($persona);
    		}
    		return $persona;
    	}
     
    	public function delete(PersonaInterface &$persona){
    		if($this->isRepositoryUsed()){
    			$this->repository->delete($persona);
    		}
    		/*
    		 * destroy object
    		 */
    		$persona = null;
    	}
    }

  3. Не исключается возможность, когда сервис может использоваться без необходимости в сохранении данных. С Proxy это уже обеспечено – достаточно просто не устанавливать реальный репозиторий. Без Заместителя, для поддержки такой логики, потребовались бы свой способ конфигурации подобного поведения и дополнительные проверки.

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

Эта запись была опубликована в рубрике PHP, Статьи-заметки и отмечена метками , , , . Добавить в закладки ссылку.

Оставить комментарий

Ваш email не будет опубликован. Обязательные поля отмечены *

Вы можете использовать это HTMLтеги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>