Статьи

Шаблон нулевого объекта — полиморфизм в моделях предметной области

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

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

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

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

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

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

Обработка неполиморфных условий

Как и следовало ожидать, при демонстрации тонкостей шаблона Null Object существует несколько путей. Одна из них, которую я нахожу особенно простой, — это реализация средства отображения данных, которое может в конечном итоге вернуть нулевое значение из общего поиска. Допустим, нам удалось создать модель скелетного домена, состоящую из одного единственного пользовательского объекта. Интерфейс вместе с его классом выглядит так:

<?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(); } 
 <?php namespace Model; class User implements UserInterface { private $id; private $name; private $email; public function __construct($name, $email) { $this->setName($name); $this->setEmail($email); } 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 ID for this user 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 = $name; 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; } } 

Класс User — это реактивная структура, которая реализует некоторые мутаторы / средства доступа для определения данных и поведения нескольких пользователей.

С этим искусственным классом домена теперь мы можем пойти дальше и определить базовый Data Mapper, который будет держать нашу модель домена и уровень доступа к данным изолированными друг от друга.

 <?php namespace ModelMapper; use LibraryDatabaseDatabaseAdapterInterface, ModelUser; class UserMapper implements UserMapperInterface { private $adapter; public function __construct(DatabaseAdapterInterface $adapter) { $this->adapter = $adapter; } public function fetchById($id) { $this->adapter->select("users", array("id" => $id)); if (!$row = $this->adapter->fetch()) { return null; } return $this->createUser($row); } private function createUser(array $row) { $user = new User($row["name"], $row["email"]); $user->setId($row["id"]); return $user; } } 

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

 <?php use LibraryLoaderAutoloader, LibraryDatabasePdoAdapter, ModelMapperUserMapper; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $adapter = new PdoAdapter("mysql:dbname=test", "myusername", "mypassword"); $userMapper = new UserMapper($adapter); $user = $userMapper->fetchById(1); if ($user !== null) { echo $user->getName() . " " . $user->getEmail(); } 

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

Удаление условных выражений из кода клиента

Однако в этом нет необходимости, так как именно в этом случае паттерн Null Object показывает, почему полиморфизм является находкой. Если мы хотим раз и навсегда избавиться от этих надоедливых условных выражений, мы можем реализовать полиморфную версию предыдущего класса User .

 <?php namespace Model; class NullUser implements UserInterface { public function setId($id) { } public function getId() { } public function setName($name) { } public function getName() { } public function setEmail($email) { } public function getEmail() { } } 

Если вы ожидаете, что полноценный класс сущностей будет упаковывать все виды наворотов, боюсь, вы сильно разочаруетесь. «Нулевая» версия объекта соответствует соответствующему интерфейсу, но методы являются пустыми обертками без фактической реализации.

Хотя существование класса NullUser очевидно, не дает нам ничего полезного для похвалы, это изящное существо, которое позволяет нам выбросить все предыдущие условные выражения в корзину. Хотите посмотреть как?

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

 <?php namespace ModelMapper; use LibraryDatabaseDatabaseAdapterInterface, ModelUser, ModelNullUser; class UserMapper implements UserMapperInterface { private $adapter; public function __construct(DatabaseAdapterInterface $adapter) { $this->adapter = $adapter; } public function fetchById($id) { $this->adapter->select("users", array("id" => $id)); return $this->createUser($this->adapter->fetch()); } private function createUser($row) { if (!$row) { return new NullUser; } $user = new User($row["name"], $row["email"]); $user->setId($row["id"]); return $user; } } 

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

 <?php $user = $userMapper->fetchById("This ID is invalid..."); echo $user->getName() . " " . $user->getEmail(); 

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

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

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

 <?php namespace Model; class NullUser implements UserInterface { public function setId($id) { } public function getId() { return "The requested ID does not correspond to a valid user."; } public function setName($name) { } public function getName() { return "The requested name does not correspond to a valid user."; } public function setEmail($email) { } public function getEmail() { return "The requested email does not correspond to a valid user."; } } 

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

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

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

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

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

Изображение через Fotolia