Статьи

Отделение интерфейсов от реализации — использование отдельных интерфейсов

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

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

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

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

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

Преступление в Запретной Территории — Вызов логики картографирования данных из доменной модели

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

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

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

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

<?php namespace Model; class AbstractEntity { protected $id; //set values for protected/private fields via the corresponding mutators public function __set($field, $value) { $this->checkField($field); $mutator = "set" . ucfirst(strtolower($field)); method_exists($this, $mutator) && is_callable(array($this, $mutator)) ? $this->$mutator($value) : $this->$field = $value; return $this; } // get values from protected/private fields via the corresponding accessors public function __get($field) { $this->checkField($field); $accessor = "get" . ucfirst(strtolower($field)); return method_exists($this, $accessor) && is_callable(array($this, $accessor)) ? $this->$accessor() : $this->$field; } protected function checkField($field) { if (!property_exists($this, $field)) { throw new InvalidArgumentException( "Setting or getting the field '$field'j is not valid for this entity."); } } // sanitize strings assigned to the fields of the entity protected function sanitizeString($value, $min = 2, $max = null) { if (!is_string($value) || strlen($value) < (integer) $min || ($max) ? strlen($value) > (integer) $max : false) { throw new InvalidArgumentException( "The value of the field accessed must be a valid string."); } return htmlspecialchars(trim($value), ENT_QUOTES); } // handle IDs public function setId($id) { if ($this->id !== null) { throw new BadMethodCallException( "The ID for this entity has been set already."); } if (!is_int($id) || $id < 1) { throw new InvalidArgumentException( "The ID of this entity is invalid."); } $this->id = $id; return $this; } public function getId() { return $this->id; } } 

Принимая во внимание, что цель здесь состоит в том, чтобы реализовать довольно прототипную модель, функциональность ядра которой может быть легко расширена по желанию, вышеупомянутый класс AbstractEntity ведет себя почти как супертип Layer, который инкапсулирует логику, общую для всех подклассов домена, которые могут случайно быть получены далее в Дорога. Это включает в себя выполнение нескольких основных задач, таких как обработка идентификаторов, очистка строк и сопоставление вызовов к мутаторам / __set() доступа с помощью __set() и __get() .

С базовым классом теперь довольно просто создать дополнительные подклассы, которые определяют данные и поведение конкретных объектов домена. Как насчет того, чтобы настроить несколько человек, которым поручено моделировать классические отношения в блогах / комментариях?

 <?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 getComments(); } 
 <?php namespace Model; interface CommentFinderInterface { public function findById($id); public function findAll(array $conditions = array()); } 
 <?php namespace Model; class Post extends AbstractEntity implements PostInterface { protected $title; protected $content; protected $comments; protected $commentFinder; public function __construct($title, $content, CommentFinderInterface $commentFinder) { $this->setTitle($title); $this->setContent($content); $this->commentFinder = $commentFinder; } public function setTitle($title) { $this->title = $this->sanitizeString($title); return $this; } public function getTitle() { return $this->title; } public function setContent($content) { $this->content = $this->sanitizeString($content); return $this; } public function getContent() { return $this->content; } public function getComments() { if ($this->comments === null) { $this->comments = $this->commentFinder->findAll( array("post_id" => $this->id)); } return $this->comments; } } 

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

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

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

 <?php namespace Model; interface CommentInterface { public function setId($id); public function getId(); public function setContent($content); public function getContent(); public function setAuthor($author); public function getAuthor(); } 
 <?php namespace Model; class Comment extends AbstractEntity implements CommentInterface { protected $content; protected $author; public function __construct($content, $author) { $this->setContent($content); $this->setAuthor($author); } public function setContent($content) { $this->content = $this->sanitizeString($content); return $this; } public function getContent() { return $this->content; } public function setAuthor($author) { $this->author = $this->sanitizeString($author); return $this; } public function getAuthor() { return $this->author; } } 

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

Получение комментариев из хранилища через Data Mapper

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

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

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

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

 <?php namespace Mapper; use LibraryDatabaseDatabaseAdapterInterface, ModelCommentFinderInterface, ModelNullComment, ModelComment; class CommentMapper implements CommentFinderInterface { protected $adapter; protected $entityTable = "comments"; public function __construct(DatabaseAdapterInterface $adapter) { $this->adapter = $adapter; } public function findById($id) { $this->adapter->select($this->entityTable, array("id" => $id)); if (!$row = $this->adapter->fetch()) { return null; } return $this->loadComment($row); } public function findAll(array $conditions = array()) { $this->adapter->select($this->entityTable, $conditions); $rows = $this->adapter->fetchAll(); return $this->loadComments($rows); } protected function loadComment(array $row) { return new Comment($row["content"], $row["author"]); } protected function loadComments(array $rows) { $comments = array(); foreach ($rows as $row) { $comments[] = $this->loadComment($row); } return $comments; } } 

Для краткости я решил оставить реализацию CommentMapper простой оболочкой для findById() и findAll() . Однако, если вы чувствуете себя предприимчивым и хотите добавить некоторые дополнительные методы, позволяющие вставлять, обновлять и удалять комментарии из связанной таблицы, продолжайте и получайте удовольствие, используя свои замечательные навыки кодирования.

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

Конечно, весь этот разговор мог бы звучать как много болтовни, если бы я не подкрепил это конкретным примером.

 <?php use LibraryLoaderAutoloader, LibraryDatabasePdoAdapter, MapperCommentMapper, ModelPost, ModelComment; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $adapter = new PdoAdapter("mysql:dbname=test", "fancyusername", "hardtoguesspassword"); $commentMapper = new CommentMapper($adapter); $post = new Post("A naive sample post", "This is the content of the sample post", $commentMapper); $post->id = 1; echo $post->title . " " . $post->content . "<br>"; foreach ($post->comments as $comment) { echo $comment->content . " " . $comment->author . "<br>"; } 

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

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

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

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

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

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