Статьи

Построение доменной модели — введение в персистентный агностицизм

Поскольку за последние несколько лет под мостом Моделей Доменов протекает так много воды, довольно трудно углубиться в ключевые концепции, не создавая еще большей путаницы в уже взволнованном ручье. Кроме того, с множеством реализаций MVC, распространяющихся как голодные муравьи, аббревиатура «М» продолжает страдать от симптомов специального уровня, обычно известного как Модель базы данных, который может полностью безнаказанно загрязнять логику домена кодом для базы данных. доступ (следовательно, инфраструктура), или с другим типом базового хранилища.

Что делает модель базы данных такой привлекательной во многих случаях, так это то, что она работает довольно хорошо с точки зрения клиентского кода. В конце концов, он легко потребляемый, поскольку скрывает много сложности за явно безвредным API (например, что-то вроде $user->save() ). Недостатком является то, что он сталкивается с шумом, когда речь идет о применении хороших методов объектно-ориентированного проектирования, не говоря уже о множестве проблем масштабируемости и тестируемости, которые в конечном итоге всплывают на поверхность.

С этой точки зрения может показаться, что популярные архитектурные шаблоны источников данных, такие как Active Record и Table Data Gateway, следует рассматривать как потенциально опасные злоумышленники, когда они связаны с логикой домена. Но обвинение шаблонов в том, что они намеревались сделать, является не чем иным, как слабым оправданием для того, чтобы не принять модель предметной области в соответствии с целью, которую она была задумана в первую очередь: независимый, независимый от настойчивости слой, ответственный за четкое определение взаимодействие между объектами системы через данные и поведение.

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

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

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

Построение базовой модели домена блога

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

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

Вот первый:

 <?php namespace Model; interface PostInterface { public function setId($id); public function getId(); public function setTitle($title); public function getTitle(); public function setContent($content); public function getContent(); public function setComments(array $comments); public function getComments(); } 

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

Поскольку код интерфейса говорит сам за себя, давайте сделаем еще один шаг и создадим другой интерфейс, чтобы указать контракт для соответствующих комментариев:

 <?php namespace Model; interface CommentInterface { public function setId($id); public function getId(); public function setContent($content); public function getContent(); public function setUser(UserInterface $user); public function getUser(); } 

Как и в PostInterface , о CommentInterface . Он опускает в модель простой контракт для объектов комментариев блога. Вполне возможно, что единственная деталь, на которую стоит обратить внимание в этом случае, — это подпись его setUser() , который обращается к setUser() Interface Injection для привязки пользователя к конкретному комментарию.

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

 <?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 setUrl($url); public function getUrl(); } 

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

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

Моделирование сообщений блога, комментариев и пользователей с помощью ПОПО

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

 <?php namespace Model; abstract class AbstractEntity { /** * Map the setting of non-existing fields to a mutator when * possible, otherwise use the matching field */ public function __set($name, $value) { $field = "_" . strtolower($name); if (!property_exists($this, $field)) { throw new InvalidArgumentException( "Setting the field '$field' is not valid for this entity."); } $mutator = "set" . ucfirst(strtolower($name)); if (method_exists($this, $mutator) && is_callable(array($this, $mutator))) { $this->$mutator($value) } else { $this->$field = $value; } return $this; } /** * Map the getting of non-existing properties to an accessor when * possible, otherwise use the matching field */ public function __get($name) { $field = "_" . strtolower($name); if (!property_exists($this, $field)) { throw new InvalidArgumentException( "Getting the field '$field' is not valid for this entity."); } $accessor = "get" . ucfirst(strtolower($name)); return (method_exists($this, $accessor) && is_callable(array($this, $accessor))) ? $this->$accessor() : $this->field; } /** * Get the entity fields */ public function toArray() { return get_object_vars($this); } } 

Я не сторонник того, чтобы сильно полагаться на магические методы PHP, но в этом случае __set() и __get() пригодятся для сокращения вызовов к установщикам и получателям, не загромождая слишком много API модели. Когда предыдущий родительский класс выполняет работу за кулисами, когда дело доходит до работы с полями объекта домена, создание конкретных реализаций для записей блога, комментариев и пользовательских объектов сводится к подклассу родительского объекта следующим образом:

 <?php namespace Model; class Post extends AbstractEntity implements PostInterface { protected $_id; protected $_title; protected $_content; protected $_comments; public function __construct($title, $content, array $comments = array()) { // map post fields to the corresponding mutators $this->setTitle($title); $this->setContent($content); if ($comments) { $this->setComments($comments); } } public function setId($id) { if ($this->_id !== null) { throw new BadMethodCallException( "The ID for this post has been set already."); } if (!is_int($id) || $id < 1) { throw new InvalidArgumentException("The post ID is invalid."); } $this->_id = $id; return $this; } public function setTitle($title) { if (!is_string($title) || strlen($title) < 2 || strlen($title) > 100) { throw new InvalidArgumentException("The post title is invalid."); } $this->_title = htmlspecialchars(trim($title), ENT_QUOTES); return $this; } public function getTitle() { return $this->_title; } public function setContent($content) { if (!is_string($content) || strlen($content) < 2) { throw new InvalidArgumentException("The post content is invalid."); } $this->_content = htmlspecialchars(trim($content), ENT_QUOTES); return $this; } public function getContent() { return $this->_content; } public function setComments(array $comments) { foreach ($comments as $comment) { if (!$comment instanceof CommentInterface) { throw new InvalidArgumentException( "One or more comments are invalid."); } } $this->_comments = $comments; return $this; } public function getComments() { return $this->_comments; } } 

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

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

 <?php namespace Model; class Comment extends AbstractEntity implements CommentInterface { protected $_id; protected $_content; protected $_user; public function __construct($content, UserInterface $user) { $this->setContent($content); $this->setUser($user); } public function setId($id) { if ($this->_id !== null) { throw new BadMethodCallException( "The ID for this comment has been set already."); } if (!is_int($id) || $id < 1) { throw new InvalidArgumentException("The comment ID is invalid."); } $this->_id = $id; return $this; } public function getId() { return $this->_id; } public function setContent($content) { if (!is_string($content) || strlen($content) < 2) { throw new InvalidArgumentException( "The content of the comment is invalid."); } $this->_content = htmlspecialchars(trim($content), ENT_QUOTES); return $this; } public function getContent() { return $this->_content; } public function setUser(UserInterface $user) { $this->_user = $user; return $this; } public function getuser() { return $this->_user; } } 
 <?php namespace Model; class User extends AbstractEntity implements UserInterface { protected $_id; protected $_name; protected $_email; protected $_url; public function __construct($name, $email, $url = null) { // map user fields to the corresponding mutators $this->setName($name); $this->setEmail($email); if ($url !== null) { $this->setUrl($url); } } 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 setUrl($url) { if (!filter_var($url, FILTER_VALIDATE_URL)) { throw new InvalidArgumentException("The user URL is invalid."); } $this->_url = $url; return $this; } public function getUrl() { return $this->_url; } } 

Миссия выполнена!

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

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

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

Внедрение модели предметной области в работу

Если мы планируем использовать многоуровневую перспективу в дизайне блога, приложение может быть легко разбито на четко определенные слои. Первый уровень — это типичный загрузчик, который просто включает и инициализирует автозагрузчик, например так:

 <?php require_once __DIR__ . "/Autoloader.php"; $autoloader = new Autoloader(); $autoloader->register(); 

Бутстрап не делает ничего странного; он просто загружает и регистрирует PSR-0-совместимый автозагрузчик.

Второй уровень будет домом для нашей доменной модели, которая на самом базовом уровне может быть реализована следующим образом:

 <?php use ModelPost, ModelComment, ModelUser; // create some posts $postOne = new Post( "Welcome to SitePoint", "To become yourself a true PHP master, yeap you must first master PHP."); $postTwo = new Post( "Welcome to SitePoint (Reprise)", "To become yourself a PHP Master, yeap you must first master... Wait! Did I post that already?"); // create a user $user = new User( "Everchanging Joe", "[email protected]"); // add some comments to the first post $postOne->comments = array( new Comment( "I just love this post! Looking forward to seeing more of this stuff.", $user), new Comment( "I just changed my mind and dislike this post! Hope not seeing more of this stuff.", $user)); // add another comment to the second post $postTwo->comments = array( new Comment( "Not quite sure if I like this post or not, so I cannot say anything for now.", $user)); 

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

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

Давайте продолжим и создадим прикладной уровень блога (контроллеры в стеке MVC), который отвечает за извлечение данных модели и передачу их на уровень представления.

Вот как выглядит этот уровень:

 <?php $posts = array($postOne, $postTwo); 

Может ли этот слой быть проще или короче? Я так не думаю.

Оставляя в стороне все насмешки, это в двух словах демонстрирует, что модель предметной области, пожалуй, самый яркий пример мантры Fat Models / Skinny Controllers в действии. Поскольку все внимание уделяется бизнес-логике, контроллеры естественным образом уменьшаются до уровня простых посредников между моделью и пользовательским интерфейсом.

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

 <!doctype html> <html> <head> <meta charset="utf-8"> <title>Building a Domain Model in PHP</title> </head> <body> <header> <h1>SitePoint.com</h1> </header> <section> <ul> <?php foreach ($posts as $post) { ?> <li> <h2><?php echo $post->title;?></h2> <p><?php echo $post->content;?></p> <?php if ($post->comments) { ?> <ul> <?php foreach ($post->comments as $comment) { ?> <li> <h3><?php echo $comment->user->name;?> says:</h3> <p><?php echo $comment->content;?></p> </li> <? } ?> </ul> <?php } ?> </li> <?php } ?> </ul> </section> </body> </html> 

Учитывая банальную ответственность уровня представления блога, я решил реализовать его, используя только собственные возможности шаблонов PHP. Несмотря на это, наиболее важным моментом, который следует здесь подчеркнуть, является тот факт, что подключение нашей модели предметной области к различным уровням было действительно легким достижением. Более того, для всей реализации не требовалось привязывать данную модель к какой-либо форме персистентной инфраструктуры, превращая ее в легко переносимое и масштабируемое существо!

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

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

Последние мысли

С огромным количеством платформ HTTP, набирающих обороты в мире PHP (таких как Symfony 2.x , Aura и даже Zend Framework ), которые не предоставляют пользователям базовую модель заранее (или, что еще хуже, предоставляют печально известную модель базы данных), надеюсь, в ближайшем будущем мы увидим больше сторонников богатых доменных моделей. Тем временем, довольно полезно подробно рассмотреть их и посмотреть, как реализовать тривиальный с нуля, как мы только что делали раньше.

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

Изображение через kentoh / Shutterstock