Статьи

Обработка коллекций совокупных корней — шаблон репозитория

Одним из наиболее типичных аспектов традиционных архитектур доменного управления (DDD) является императивный постоянный агностицизм, раскрываемый моделью предметной области . В более консервативных проектах, включая несколько реализаций, основанных на Active Record или Data Table Gateway (которые из-за довольно обманчивой простоты часто приводят к отравлению логики домена инфраструктурой), всегда существует явное представление о базовом механизме хранения, живущем и подавляющем строка, как правило, реляционная база данных. С другой стороны, доменные модели с самого начала концептуально разрабатываются в жесткой «незапоминающей» природе, тем самым вытесняя любую логику персистентности из своих границ.

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

Помимо всего прочего , справедливо признать, что иметь дело со странностями слоя Data Mappers — это довольно обременительное занятие , которое часто сводится к стратегии «кодируй один раз / используй навсегда». Несмотря на то, что приведенная выше схема работает прилично хорошо в довольно упрощенных условиях, когда имеется всего несколько классов доменов, обрабатываемых небольшим числом картографов. Ситуация может стать намного более неловкой, однако, когда модель начинает увеличиваться в размерах и усложняется, поскольку со временем будут обязательно добавлены дополнительные картографы.

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

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

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

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

Делать некоторые предварительные основы

Процесс внедрения репозитория может быть довольно сложным, потому что он фактически скрывает все основные моменты внедрения и обработки Data Mappers за упрощенным API, подобным коллекции, который, в свою очередь, также внедряет какой-то тип адаптера персистентности и так далее. Это последовательное внедрение зависимостей в сочетании с сокрытием обширной логики объясняет, почему хранилище часто считается простым фасадом , даже если некоторые мнения в настоящее время расходятся с этой концепцией.

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

<?php namespace Model; interface UserInterface { public function setId($id); public function getId(); public function setName($name); public function getName(); public function setEmail($email); public function getEmail(); public function setRole($role); public function getRole(); } 
 <?php namespace Model; class User implements UserInterface { const ADMINISTRATOR_ROLE = "Administrator"; const GUEST_ROLE = "Guest"; protected $id; protected $name; protected $email; protected $role; public function __construct($name, $email, $role = self::GUEST_ROLE) { $this->setName($name); $this->setEmail($email); $this->setRole($role); } public function setId($id) { if ($this->id !== null) { throw new BadMethodCallException( "The ID for this user has been set already."); } if (!is_int($id) || $id < 1) { throw new InvalidArgumentException( "The user ID is invalid."); } $this->id = $id; return $this; } public function getId() { return $this->id; } public function setName($name) { if (strlen($name) < 2 || strlen($name) > 30) { throw new InvalidArgumentException( "The user name is invalid."); } $this->name = htmlspecialchars(trim($name), ENT_QUOTES); return $this; } public function getName() { return $this->name; } public function setEmail($email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException( "The user email is invalid."); } $this->email = $email; return $this; } public function getEmail() { return $this->email; } public function setRole($role) { if ($role !== self::ADMINISTRATOR_ROLE && $role !== self::GUEST_ROLE) { throw new InvalidArgumentException( "The user role is invalid."); } $this->role = $role; return $this; } public function getRole() { return $this->role; } } 

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

Поскольку модель уже работает в своей изолированной среде, давайте сделаем ее немного более богатой, добавив в нее дополнительный класс, отвечающий за обработку коллекций пользовательских объектов. Этот компонент «addendum» является просто классической оболочкой массива, реализующей интерфейсы SPL ArrayAccess , ArrayAccess и IteratorAggregate :

 <?php namespace ModelCollection; use MapperUserCollectionInterface, ModelUserInterface; class UserCollection implements UserCollectionInterface { protected $users = array(); public function add(UserInterface $user) { $this->offsetSet($user); } public function remove(UserInterface $user) { $this->offsetUnset($user); } public function get($key) { return $this->offsetGet($key); } public function exists($key) { return $this->offsetExists($key); } public function clear() { $this->users = array(); } public function toArray() { return $this->users; } public function count() { return count($this->users); } public function offsetSet($key, $value) { if (!$value instanceof UserInterface) { throw new InvalidArgumentException( "Could not add the user to the collection."); } if (!isset($key)) { $this->users[] = $value; } else { $this->users[$key] = $value; } } public function offsetUnset($key) { if ($key instanceof UserInterface) { $this->users = array_filter($this->users, function ($v) use ($key) { return $v !== $key; }); } else if (isset($this->users[$key])) { unset($this->users[$key]); } } public function offsetGet($key) { if (isset($this->users[$key])) { return $this->users[$key]; } } public function offsetExists($key) { return ($key instanceof UserInterface) ? array_search($key, $this->users) : isset($this->users[$key]); } public function getIterator() { return new ArrayIterator($this->users); } } с <?php namespace ModelCollection; use MapperUserCollectionInterface, ModelUserInterface; class UserCollection implements UserCollectionInterface { protected $users = array(); public function add(UserInterface $user) { $this->offsetSet($user); } public function remove(UserInterface $user) { $this->offsetUnset($user); } public function get($key) { return $this->offsetGet($key); } public function exists($key) { return $this->offsetExists($key); } public function clear() { $this->users = array(); } public function toArray() { return $this->users; } public function count() { return count($this->users); } public function offsetSet($key, $value) { if (!$value instanceof UserInterface) { throw new InvalidArgumentException( "Could not add the user to the collection."); } if (!isset($key)) { $this->users[] = $value; } else { $this->users[$key] = $value; } } public function offsetUnset($key) { if ($key instanceof UserInterface) { $this->users = array_filter($this->users, function ($v) use ($key) { return $v !== $key; }); } else if (isset($this->users[$key])) { unset($this->users[$key]); } } public function offsetGet($key) { if (isset($this->users[$key])) { return $this->users[$key]; } } public function offsetExists($key) { return ($key instanceof UserInterface) ? array_search($key, $this->users) : isset($this->users[$key]); } public function getIterator() { return new ArrayIterator($this->users); } } 

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

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

 <?php namespace Mapper; use ModelUserInterface; interface UserCollectionInterface extends Countable, ArrayAccess, IteratorAggregate { public function add(UserInterface $user); public function remove(UserInterface $user); public function get($key); public function exists($key); public function clear(); public function toArray(); } 
 <?php namespace Mapper; use ModelRepositoryUserMapperInterface, ModelUser; class UserMapper implements UserMapperInterface { protected $entityTable = "users"; protected $collection; public function __construct(DatabaseAdapterInterface $adapter, UserCollectionInterface $collection) { $this->adapter = $adapter; $this->collection = $collection; } public function fetchById($id) { $this->adapter->select($this->entityTable, array("id" => $id)); if (!$row = $this->adapter->fetch()) { return null; } return $this->createUser($row); } public function fetchAll(array $conditions = array()) { $this->adapter->select($this->entityTable, $conditions); $rows = $this->adapter->fetchAll(); return $this->createUserCollection($rows); } protected function createUser(array $row) { $user = new User($row["name"], $row["email"], $row["role"]); $user->setId($row["id"]); return $user; } protected function createUserCollection(array $rows) { $this->collection->clear(); if ($rows) { foreach ($rows as $row) { $this->collection[] = $this->createUser($row); } } return $this->collection; } } 

«Из коробки» пакет задач, выполняемых UserMapper , довольно прост: он ограничен раскрытием пары общих UserMapper которые UserMapper за UserMapper пользователей из базы данных и реконструкцию соответствующих объектов с помощью createUser() . Более того, если вы уже погрузили свои зубы в несколько картографов раньше или даже написали свои собственные шедевры картографирования, несомненно, вышеприведенное должно быть довольно легко понять. Вполне возможно, что единственная тонкая деталь, на которую стоит обратить внимание, это то, что UserCollectionInterface был помещен в слой отображения, а не в модель. В этом случае я решил сделать это преднамеренно, так как абстракция (протокол), от которой зависит коллекция пользователей, явно объявлена ​​и принадлежит высокому уровню UserMapper в соответствии с рекомендациями, продвигаемыми Принципом инверсии зависимостей. ,

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

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

Реализация пользовательского репозитория

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

 <?php namespace ModelRepository; interface UserMapperInterface { public function fetchById($id); public function fetchAll(array $conditions = array()); } 
 <?php namespace ModelRepository; interface UserRepositoryInterface { public function fetchById($id); public function fetchByName($name); public function fetchbyEmail($email); public function fetchByRole($role); } 
 <?php namespace ModelRepository; class UserRepository implements UserRepositoryInterface { protected $userMapper; public function __construct(UserMapperInterface $userMapper) { $this->userMapper = $userMapper; } public function fetchById($id) { return $this->userMapper->fetchById($id); } public function fetchByName($name) { return $this->fetch(array("name" => $name)); } public function fetchByEmail($email) { return $this->fetch(array("email" => $email)); } public function fetchByRole($role) { return $this->fetch(array("role" => $role)); } protected function fetch(array $conditions) { return $this->userMapper->fetchAll($conditions); } } 

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

В более реалистичной среде хранилище также должно иметь возможность сохранения совокупных корней. Если вы хотите передать метод insert() или что-то еще по этой линии в UserRepository , не стесняйтесь делать это.

В любом случае одним из эффективных способов уловить реальные преимущества использования репозитория является пример.

 <?php use LibraryLoaderAutoloader, LibraryDatabasePdoAdapter, MapperUserMapper, ModelCollectionUserCollection, ModelRepositoryUserRepository; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $adapter = new PdoAdapter("mysql:dbname=users", "myfancyusername", "mysecretpassword"); $userRepository = new UserRepository(new UserMapper($adapter, new UserCollection())); $users = $userRepository->fetchByName("Rachel"); foreach ($users as $user) { echo $user->getName() . " " . $user->getEmail() . "<br>"; } $users = $userRepository->fetchByEmail("[email protected]"); foreach ($users as $user) { echo $user->getName() . " " . $user->getEmail() . "<br>"; } $administrators = $userRepository->fetchByRole("administrator"); foreach ($administrators as $administrator) { echo $administrator->getName() . " " . $administrator->getEmail() . "<br>"; } $guests = $userRepository->fetchByRole("guest"); foreach ($guests as $guest) { echo $guest->getName() . " " . $guest->getEmail() . "<br>"; } 

Как отмечалось ранее, хранилище эффективно обменивается бизнес-терминологией с клиентским кодом (так называемый повсеместный язык, придуманный Эриком Эвансом в его книге « Конструкция, управляемая доменом» ), а не с техническим уровнем более низкого уровня. В отличие от неоднозначности, присутствующей в средствах поиска картографических данных, методы хранилища, с другой стороны, описывают себя в терминах «имя», «электронная почта» и «роль», которые, безусловно, являются частью атрибутов, которые моделируют сущности пользователей.

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

Заключительные мысли

Будучи одной из центральных концепций доменного дизайна, хранилища можно найти в приложениях, написанных на нескольких других языках, таких как Java и C #, и это лишь некоторые из них. В PHP, однако, они все еще относительно неизвестны, просто делают свои первые робкие шаги в мире. Несмотря на это, существует несколько надежных сред, таких как FLOW3 и, конечно, Doctrine 2.x , которые помогут вам принять парадигму DDD.

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

Изображение с помощью Chance Agrella / Freerangestock.com