Хотя, по общему признанию, концепция исходит из эмпирической области, а не из академической, она вполне применима к прагматизму реального мира — большинство программных приложений ведут себя почти как причудливые дети с неизбежной «тенденцией» расти в размерах и сложности с течением времени. Нет ничего плохого в том, что дети растут здесь и там здоровыми, но ситуация с приложениями может радикально отличаться, особенно когда они становятся раздутыми, извращенными монстрами, имеющими мало общего с милыми, крошечными существами, которыми они когда-то были.
Проблема не в росте как таковом, поскольку наличие перспективного приложения, расправляющего свои крылья в сторону дальнейших горизонтов, может быть признаком хорошего дизайна. Реальная проблема заключается в том, когда процесс расширения достигается за счет избыточных стандартных реализаций вещей, разбросанных по разным уровням. Мы все были там ( mea culpa ), и мы все знаем, что логическое дублирование — серьезная программная болезнь.
Даже когда надежные методы программирования помогают убрать дублированную логику, иногда их просто недостаточно, чтобы решить проблему самостоятельно. Ярким примером этого является MVC; модель (модель предметной области , а не навязчивая, всеохватывающая база данных) делает свою работу в непринужденной изоляции, затем узкие контроллеры захватывают данные модели через маппер или репозиторий, передают их в представление или любой другой обработчик вывода для дальнейшего рендеринг. Схема может доставить, но она плохо масштабируется.
Скажем, данные модели должны быть обработаны каким-то дополнительным способом и подключены к внешнему API, такому как Facebook, Twitter, сторонний почтовик, назовите его, в нескольких местах . В таких случаях весь процесс массирования / сопряжения является логикой приложения сверху вниз, следовательно, ответственность контроллеров. Прежде чем вы это знаете, вы вынуждены продублировать одну и ту же логику на нескольких контроллерах, тем самым поставив свои пальцы на запрещенную область логического дублирования. Busted!
Если вы похожи на меня, вы, вероятно, задаетесь вопросом, как решить проблему, не ударяя головой о кирпичную стену. На самом деле, есть несколько подходов, которые делают работу достаточно хорошо. Один из них мне особенно нравится, потому что он хорошо сочетается с моделями доменов и, следовательно, с дизайном на основе доменов . И более того, если вы заглянули в заголовок статьи, вы, вероятно, догадались, что я имею в виду Сервисы.
Что такое сервис?
Сервисы — это уровень абстракции, размещенный поверх модели предметной области, который инкапсулирует общую логику приложения в едином API, так что он может легко использоваться различными уровнями клиента.
Не позволяйте определению волновать вас, как будто вы уже некоторое время используете MVC, скорее всего, вы уже использовали сервис. Контроллеры часто называют сервисами, так как они выполняют логику приложения и дополнительно могут взаимодействовать с несколькими уровнями клиента, а именно с представлениями. Конечно, в более требовательной среде простые контроллеры терпят неудачу при обработке нескольких клиентов без вышеупомянутого дублирования, поэтому в таких случаях более подходящим является создание отдельного слоя.
Создание упрощенной доменной модели
Сервисы являются настоящими убийцами при работе с приложениями корпоративного уровня, магистраль которых опирается на опоры богатой модели предметной области и где взаимодействие с несколькими клиентами является правилом, а не исключением. Это не означает, что вы просто не можете использовать сервис для своего следующего проекта в блоге для домашних животных, потому что на самом деле вы можете, и, скорее всего, никто не накажет вас за такие эпические усилия.
Я должен признать, что мои собственные слова вернутся, чтобы преследовать меня, рано или поздно, хотя, поскольку мой «непослушный» план заключается в том, чтобы создать сервис с нуля, который будет связывать данные модели предметной области, состоящие из нескольких пользовательских объектов, с двумя независимые клиентские слои. Звучит излишне, конечно, но, надеюсь, в конце концов дидактичен.
Следующая диаграмма в двух словах показывает, как эта экспериментальная служба будет функционировать на самом базовом уровне:
Поведение службы довольно тривиально. Его ответственность может сводиться к простому извлечению данных из модели предметной области, которая затем будет закодирована / сериализована в JSON и предоставлена нескольким клиентам (клиентский уровень A и клиентский уровень B). Прелесть этой схемы в том, что вся логика кодирования / сериализации будет жить и дышать за API класса обслуживания, размещенного в верхней части модели, таким образом защищая приложение от избыточных проблем с реализацией, оставляя при этом дверь открытой для подключения дополнительных клиентов. дальше по дороге.
Предполагая, что создание службы будет проходить снизу вверх, первый уровень, который будет построен, — это уровень доступа к данным (DAL). И поскольку задачи, связанные с инфраструктурой, будут ограничены хранением / извлечением данных модели из базы данных, этот слой будет выглядеть почти так же, как тот, который я написал в предыдущей статье .
Когда DAL делает свое дело, давайте сделаем еще один шаг и начнем изучать модель предметной области. Этот будет довольно примитивным, с задачей моделирования общих пользователей:
<?php namespace Model; abstract class AbstractEntity { public function __set($field, $value) { if (!property_exists($this, $field)) { throw new InvalidArgumentException( "Setting the field '$field' is not valid for this entity."); } $mutator = "set" . ucfirst(strtolower($field)); method_exists($this, $mutator) && is_callable(array($this, $mutator)) ? $this->$mutator($value) : $this->$field = $value; return $this; } public function __get($field) { if (!property_exists($this, $field)) { throw new InvalidArgumentException( "Getting the field '$field' is not valid for this entity."); } $accessor = "get" . ucfirst(strtolower($field)); return method_exists($this, $accessor) && is_callable(array($this, $accessor)) ? $this->$accessor() : $this->$field; } public function toArray() { return get_object_vars($this); } }
<?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 setRanking($ranking); public function getRanking(); }
<?php namespace Model; class User extends AbstractEntity implements UserInterface { const LOW_POSTER = "low"; const MEDIUM_POSTER = "medium"; const TOP_POSTER = "high"; protected $id; protected $name; protected $email; protected $ranking; public function __construct($name, $email, $ranking = self::LOW_POSTER) { $this->setName($name); $this->setEmail($email); $this->setRanking($ranking); } 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 setRanking($ranking) { switch ($ranking) { case self::LOW_POSTER: case self::MEDIUM_POSTER: case self::TOP_POSTER: $this->ranking = $ranking; break; default: throw new InvalidArgumentException( "The post ranking '$ranking' is invalid."); } return $this; } public function getRanking() { return $this->ranking; } }
Помимо выполнения некоторого типичного сопоставления установщика / получателя и выполнения базовой проверки данных, поведение классов AbstractEntity
и User
не идет дальше этого. Несмотря на это, они пригодятся для того, чтобы воплотить в жизнь маленькую, но чистую модель предметной области, которую можно настроить по желанию.
Модель следует каким-то образом подключить к DAL, не теряя при этом первозданную независимость друг от друга. Одним из простых и простых способов сделать это является использование средств отображения данных.
Взаимодействие модели с DAL
Если вы когда-нибудь сталкивались с этим процессом, вы будете знать, что создание преобразователя данных с полным стеком, способного обрабатывать несколько доменных объектов и выявлять любые несоответствия импеданса на лету, не простая задача, а во многих случаях эффективный отпугиватель для даже самый смелый из кодеров. Но поскольку модель предметной области здесь довольно проста, пользовательский картограф, который я планирую развернуть, довольно прост.
<?php namespace ModelMapper; use ModelUserInterface; interface UserMapperInterface { public function fetchById($id); public function fetchAll(array $conditions = array()); public function insert(UserInterface $user); public function delete($id); }
<?php namespace ModelMapper; use LibraryDatabaseDatabaseAdapterInterface, ModelUserInterface, ModelUser; class UserMapper implements UserMapperInterface { protected $entityTable = "users"; public function __construct(DatabaseAdapterInterface $adapter) { $this->adapter = $adapter; } 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()) { $users = array(); $this->adapter->select($this->entityTable, $conditions); $rows = $this->adapter->fetchAll(); if ($rows) { foreach ($rows as $row) { $users[] = $this->createUser($row); } } return $users; } public function insert(UserInterface $user) { $user->id = $this->adapter->insert($this->entityTable, array( "name" => $user->name, "email" => $user->email, "ranking" => $user->ranking)); return $user->id; } public function delete($id) { if ($id instanceof UserInterface) { $id = $id->id; } return $this->adapter->delete($this->entityTable, array("id = $id")); } protected function createUser(array $row) { $user = new User($row["name"], $row["email"], $row["ranking"]); $user->id = $row["id"]; return $user; } }
Конечно, класс UserMapper
находится далеко от готового компонента, но он работает достойно. Короче говоря, он выполняет несколько операций CRUD на модели предметной области и восстанавливает объекты User с помощью своего createUser()
. (Я оставил пользовательские обновления в качестве упражнения для читателя, так что будьте готовы к дополнительному веселью).
Более того, поскольку маппер удобно лежит между моделью и DAL, реализация службы, которая выводит данные в формате JSON во внешний мир, теперь должна стать более гибким процессом. Как обычно, конкретные примеры кода трудно превзойти, когда речь идет о дальнейшей разработке этой концепции. Итак, давайте теперь построим сервис в несколько шагов.
Создание сменного сервисного уровня
Существует общее согласие, которое совпадает с мнением Фаулера и Эвана о том, что сервисы должны быть тонкими контейнерами, охватывающими только логику приложения. Бизнес-логика, с другой стороны, должна быть смещена внутрь границ доменной модели. И поскольку мне нравится придерживаться умных предложений, которые приходят от старших, служба здесь будет придерживаться их.
Сказав это, пришло время создать первый элемент сервисного уровня. Это базовый интерфейс, который позволит нам внедрять различные стратегии кодировщика / сериализатора во внутреннюю часть службы во время выполнения, не внося изменений в одну строку клиентского кода:
<?php namespace Service; interface EncoderInterface { public function encode(); }
При наличии вышеуказанного интерфейса создание нескольких реализаторов действительно просто. Кроме того, следующая оболочка JSON доказывает, почему мое утверждение верно:
<?php namespace Service; class JsonEncoder implements EncoderInterface { protected $data = array(); public function setData(array $data) { foreach ($data as $key => $value) { if (is_object($value)) { $array = array(); $reflect = new ReflectionObject($value); foreach ($reflect->getProperties() as $prop) { $prop->setAccessible(true); $array[$prop->getName()] = $prop->getValue($value); } $data[$key] = $array; } } $this->data = $data; return $this; } public function encode() { return array_map("json_encode", $this->data); } }
Если вам было легко понять, как JsonEncoder
делает свое дело, не забудьте проверить нижеприведенную JsonEncoder
наивного сериализатора PHP:
<?php namespace Service; class Serializer implements EncoderInterface { protected $data = array(); public function setData(array $data) { $this->data = $data; return $this; } public function encode() { return array_map("serialize", $this->data); } }
Благодаря функциональности, предоставляемой кодировщиком и сериализатором из коробки, реализация службы, которая JSON-кодирует и сериализует данные модели, теперь может быть принята с уверенностью. Вот как выглядит этот сервис:
<?php namespace Service; use ModelMapperUserMapperInterface; class UserService { protected $userMapper; protected $encoder; public function __construct(UserMapperInterface $userMapper, EncoderInterface $encoder = null) { $this->userMapper = $userMapper; $this->encoder = $encoder; } public function setEncoder(EncoderInterface $encoder) { $this->encoder = $encoder; return $this; } public function getEncoder() { if ($this->encoder === null) { throw new RuntimeException( "There is not an encoder to use."); } return $this->encoder; } public function fetchById($id) { return $this->userMapper->fetchById($id); } public function fetchAll(array $conditions = array()) { return $this->userMapper->fetchAll($conditions); } public function fetchByIdEncoded($id) { $user = $this->fetchById($id); return $this->getEncoder()->setData(array($user))->encode(); } public function fetchAllEncoded(array $conditions = array()) { $users = $this->fetchAll($conditions); return $this->getEncoder()->setData($users)->encode($users); } }
На первый взгляд кажется, что класс UserService
сидит на пользовательском картографическом устройстве с единственной целью UserService
его искатели или действовать как простой репозиторий. Но на самом деле это намного больше, так как его fetchByIdEncoded()
и fetchAllEncoded()
инкапсулируют в одном месте всю логику, необходимую для кодирования / сериализации данных модели в соответствии с кодировщиком, передаваемым конструктору или установщику.
Хотя функциональность класса ограничена, он в двух словах показывает, как построить сервисный уровень, который является посредником между моделью и парой клиентов, которые ожидают получения данных в определенном формате. Конечно, я был бы придурком, если бы не показал вам, как наконец-то начать работать с таким сервисом, поэтому в приведенном ниже примере он используется для извлечения пользователей из базы данных:
<?php use LibraryLoaderAutoloader, LibraryDatabasePdoAdapter, ModelMapperUserMapper, ServiceUserService, ServiceSerializer, ServiceJsonEncoder; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $adapter = new PdoAdapter("mysql:dbname=mydatabase", "myfancyusername", "mysecretpassword"); $userService = new UserService(new UserMapper($adapter)); $userService->setEncoder(new JsonEncoder); print_r($userService->fetchAllEncoded()); print_r($userService->fetchByIdEncoded(1)); $userService->setEncoder(new Serializer()); print_r($userService->fetchAllEncoded(array("ranking" => "high"))); print_r($userService->fetchByIdEncoded(1));
Несмотря на некоторые очевидные ограничения, ясно, что служба является гибким компонентом, который не только кодирует объекты пользователя в соответствии с некоторым заранее заданным форматом, но также позволяет добавлять больше кодеров по пути благодаря возможностям шаблона стратегии . Конечно, его наиболее привлекательным достоинством является возможность размещать общую логику приложения за чистым API, что необходимо для приложений, которым необходимо выполнять множество дополнительных централизованных задач, таких как дальнейшая обработка данных модели домена, проверка, ведение журнала и многое другое.
Резюме
Несмотря на то, что службы все еще делают свои первые робкие шаги в мейнстриме PHP (за исключением некоторых конкретных платформ, таких как FLOW3 и некоторых других сред, которые робко обеспечивают базовый план для создания служб безболезненно), они являются надежными, хорошо зарекомендовавшими себя решениями. в корпоративном мире, где системы обычно опираются на основы богатой доменной модели и взаимодействуют с широким спектром клиентских уровней.
Несмотря на этот довольно подавляющий сценарий, нет ничего, что явно мешало бы вам погрузиться в лакомство услуг в небольших, более скромных условиях, особенно если вы играете с некоторыми основными понятиями DDD. Итак, теперь, когда вы знаете, что происходит под капотом услуг, не стесняйтесь дать им шанс. Вы не пожалеете об этом.
Изображение через kentoh / Shutterstock