Виртуальные прокси, стоящие за довольно причудливым и цветочным названием, вполне возможно, являются одним из наиболее заметных примеров того, почему мантра «Программирование на интерфейсах» — это нечто большее, чем просто тупой догматический принцип. Опираясь на основу полиморфизма (динамический полиморфизм, а не случайный, часто достигаемый путем переопределения простого метода), виртуальные прокси представляют собой простую, но прочную концепцию, которая позволяет отложить построение / загрузку дорогостоящих графов объектов без необходимости изменять клиент код.
Одна из замечательных сторон прокси-серверов заключается в том, что они могут быть концептуально спроектированы для работы с отдельными объектами или с их коллекциями (или с обоими, даже если это может поставить под угрозу разделение интересов и с течением времени станет сложным в управлении). Чтобы продемонстрировать с практической точки зрения, как использовать функциональные возможности, предоставляемые виртуальными прокси-серверами, в первой части этой серии статей я рассмотрел несколько примеров, показывающих, как использовать базовый прокси-сервер для извлечения совокупности из базы данных, чтобы выполнить упрощенную модель предметной области.
Несмотря на то, что опыт был, надеюсь, дидактическим, и, если повезет, веселым, его оборотная сторона была несколько обманчива, поскольку в нем демонстрировались основные черты виртуальных прокси, а не то, как реализовать их в более реалистичном сценарии. Прокси — это сложная армада, когда речь идет о отложенной загрузке коллекций объектов домена из хранилища. Чтобы получить представление о концепции, просто подумайте о партии постов в блоге, где каждый набор связанных комментариев может быть получен по запросу из базы данных; несомненно, вы поймете, почему прокси-серверы могут превосходно выполнять то, что обещают в подобных случаях.
Как обычно, практика — лучший учитель. В этой части я покажу, как подключить прокси к определенной коллекции объектов домена. Я буду воссоздавать на самом базовом уровне этот типичный сценарий, чтобы вы могли видеть его логику вождения довольно безболезненно.
Создание коллекции доменных объектов
Как я объяснил, виртуальные прокси обычно используются для извлечения совокупных корней из уровня персистентности, которые связаны с коллекцией базовых доменных объектов. Из-за своей плодовитости по своей природе коллекции во многих случаях стоят дорого для предварительной установки, что делает их хорошими кандидатами для перетаскивания по требованию на накладные расходы, вызванные дорогостоящими поездками в базу данных.
Более того, учитывая, что связь «один ко многим» между постами в блоге и соответствующими комментариями довольно точно отражает в этом случае использования, было бы весьма поучительно сначала смоделировать отношения с помощью нескольких простых классов домена, прежде чем работать над конкретным прокси.
Чтобы было проще понять, первыми двумя игроками, которых я добавлю на этапе тестирования, будет отдельный интерфейс вместе с базовым исполнителем. Объединив усилия, они определят контракт и реализацию общих объектов блога:
<?php namespace Model; use ModelCollectionCommentCollectionInterface; 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(CommentCollectionInterface $comments); public function getComments(); }
<?php namespace Model; use ModelCollectionCommentCollectionInterface; class Post implements PostInterface { protected $id; protected $title; protected $content; protected $comments; public function __construct($title, $content, CommentCollectionInterface $comments = null) { $this->setTitle($title); $this->setContent($content); $this->comments = $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 getId() { return $this->id; } 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(CommentCollectionInterface $comments) { $this->comments = $comments; return $this; } public function getComments() { return $this->comments; } }
Понимание логики вышеупомянутого класса Post
является тривиальным процессом, который не нуждается в объяснении. Тем не менее, здесь есть важная деталь, которую стоит отметить: класс явно объявляет в конструкторе зависимость от все еще неопределенной коллекции комментариев. Давайте теперь создадим класс, который порождает комментарии:
<?php namespace Model; interface CommentInterface { public function setId($id); public function getId(); public function setContent($content); public function getContent(); public function setPoster($poster); public function getPoster(); }
<?php namespace Model; class Comment implements CommentInterface { protected $id; protected $content; protected $poster; public function __construct($content, $poster) { $this->setContent($content); $this->setPoster($poster); } 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 setPoster($poster) { if (!is_string($poster) || strlen($poster) < 2 || strlen($poster) > 30) { throw new InvalidArgumentException( "The poster is invalid."); } $this->poster = htmlspecialchars(trim($poster), ENT_QUOTES); return $this; } public function getPoster() { return $this->poster; } }
Пока что дела идут гладко. О вышеперечисленных классах доменов мало что можно сказать, за исключением того, что они представляют собой тонкие блоки базовой модели домена , где каждый объект поста блога демонстрирует связь «один ко многим» со связанными комментариями. Не стесняйтесь называть меня пуристом, если хотите, но, на мой взгляд, текущая реализация модели выглядит недоделанной и неуклюжей, если ее не приправить набором комментариев. Давайте сделаем модель немного богаче, добавив к ней логику этого дополнительного компонента:
<?php namespace ModelCollection; interface CommentCollectionInterface extends Countable, IteratorAggregate { public function getComments(); }
<?php namespace ModelCollection; use ModelCommentInterface; class CommentCollection implements CommentCollectionInterface { protected $comments = array(); public function __construct(array $comments = array()) { if ($comments) { foreach($comments as $comment) { $this->addComment($comment); } } } public function addComment(CommentInterface $comment) { $this->comments[] = $comment; return $this; } public function getComments() { return $this->comments; } public function count() { return count($this->comments); } public function getIterator() { return new ArrayIterator($this->comments); } }
Если вы CommentCollection
и просматриваете класс CommentCollection
, первое, что вы заметите, это тот факт, что это не что иное, как итеративная, счетная оболочка массива, скрытая за причудливой маскировкой. На самом деле коллекции массивов бывают разных форм и разновидностей, но в большинстве случаев они просто используются ArrayAccess
SPL Iterator
и ArrayAccess
. В этом случае я хотел уберечь себя (и вас) от решения такой скучной задачи и сделал класс реализатором IteratorAggregate
.
После того, как коллекция комментариев уже создана, мы могли бы просто сделать еще один шаг и поставить модель предметной области, чтобы сделать то, что она должна делать — помассировать несколько объектов постов в блоге тут и там, и даже связать их с партией комментариев, с нетерпением полученных из база данных. Но при этом мы просто обманывали бы себя и не использовали бы функциональность, которую Виртуальные прокси предлагают в полной мере.
Учитывая, что в типичной реализации прокси предоставляют тот же API, что и реальные объекты домена, прокси, который взаимодействует с предыдущим классом CommentCollection
должен также реализовывать CommentCollectionInterface
чтобы выполнить контракт с клиентским кодом, не добавляя куда-то кучу вонючих условий.
Взаимодействие с коллекциями доменных объектов через виртуальный прокси
Откровенно говоря, коллекции с массивами, подобные предыдущим, могут счастливо существовать сами по себе, не полагаясь на какие-либо другие зависимости. (Если вы настроены скептически, не стесняйтесь проверить, как коллекции в Doctrine делают свои вещи за кулисами.) Несмотря на это, помните, что я пытаюсь реализовать прокси-сервер, который имитирует поведение реальной коллекции комментариев, но это на самом деле является легким заменой.
Возникает вопрос: как можно извлечь комментарии из базы данных и поместить в более раннюю коллекцию? Есть несколько способов сделать это, но я считаю, что наиболее привлекательным является использование картографа данных, поскольку он способствует постоянному агностицизму.
Приведенный ниже маппер делает достойную работу по извлечению коллекций комментариев к записи из хранилища. Проверьте это:
<?php namespace ModelMapper; interface CommentMapperInterface { public function fetchById($id); public function fetchAll(array $conditions = array()); }
<?php namespace ModelMapper; use LibraryDatabaseDatabaseAdapterInterface, ModelCollectionCommentCollection, ModelComment; class CommentMapper implements CommentMapperInterface { protected $adapter; protected $entityTable = "comments"; 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->createComment($row); } public function fetchAll(array $conditions = array()) { $collection = new CommentCollection; $this->adapter->select($this->entityTable, $conditions); $rows = $this->adapter->fetchAll(); if ($rows) { foreach ($rows as $row) { $collection->addComment($this->createComment($row)); } } return $collection; } protected function createComment(array $row) { return new Comment($row["content"], $row["poster"]); } }
В то время как средства CommentMapper
классом CommentMapper
в целом придерживаются API, чего можно ожидать в стандартной реализации средства отображения данных, метод fetchAll()
безусловно, является самым ярким ребенком в блоке. Сначала он извлекает все комментарии к сообщениям в блоге из хранилища и помещает их в коллекцию, которая в итоге возвращается к клиентскому коду. Если вы похожи на меня, возможно, в вашей голове прозвенел будильник, потому что коллекция напрямую создается в методе.
На самом деле, нет необходимости быть в ярости по поводу new
операторов, живущих за пределами заводов, по крайней мере, в этом случае, поскольку коллекция на самом деле является общей структурой, которая подпадает под категорию «новых», а не «инъекционных». В любом случае, если вы чувствуете себя немного менее виноватым, вставляя коллекцию в конструктор картографа, не стесняйтесь делать это.
При наличии средства отображения комментариев пришло время пройти через настоящий момент прозрения и создать прокси-класс, который будет взаимодействовать с более ранней коллекцией:
<?php namespace ModelProxy; use ModelCollectionCommentCollectionInterface, ModelMapperCommentMapperInterface; class CommentCollectionProxy implements CommentCollectionInterface { protected $comments; protected $postId; protected $commentMapper; public function __construct($postId, CommentMapperInterface $commentMapper) { $this->postId = $postId; $this->commentMapper = $commentMapper; } public function getComments() { if ($this->comments === null) { if(!$this->comments = $this->commentMapper->fetchAll( array("post_id" => $this->postId))) { throw new UnexpectedValueException( "Unable to fetch the comments."); } } return $this->comments; } public function count() { return count($this->getComments()); } public function getIterator() { return $this->getComments(); } }
Как и следовало ожидать, CommentCollectionProxy
реализует тот же интерфейс, что и одна из реальных коллекций комментариев. Тем не менее, его getComments()
выполняет работу за кулисами и лениво загружает комментарии из базы данных через преобразователь, передаваемый в конструкторе.
Этот простой, но эффективный трюк позволяет вам делать все умные вещи с комментариями, не страдая от чрезмерного потоотделения подмышками. Хотите увидеть какие? Ну, скажем, вам нужно получить все комментарии, привязанные к конкретному сообщению в блоге, из базы данных. Следующий фрагмент выполняет свою работу:
<?php use LibraryLoaderAutoloader, LibraryDatabasePdoAdapter, ModelMapperCommentMapper, ModelProxyCommentCollectionProxy, ModelComment, ModelPost; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $adapter = new PdoAdapter("mysql:dbname=blog", "myfancyusername", "mysecretpassword"); $commentMapper = new CommentMapper($adapter); $comments = $commentMapper->fetchAll(array("post_id" => 1)); $post = new Post("The post title", "This is just a sample post.", $comments); echo $post->getTitle() . " " . $post->getContent() . "<br />"; foreach ($post->getComments() as $comment) { echo $comment->getContent() . " " . $comment->getPoster() . "<br />"; }
Недостатком этого подхода является то, что комментарии сначала извлекаются из хранилища, а затем внедряются во внутренние объекты объекта post. Как насчет обратного, но на этот раз путем «одурачивания» клиентского кода прокси-сервером?
<?php $comments = new CommentCollectionProxy(1, new CommentMapper($adapter)); $post = new Post("The post title", "This is just a sample post.", $comments); echo $post->getTitle() . " " . $post->getContent() . "<br />"; foreach ($post->getComments() as $comment) { echo $comment->getContent() . " " . $comment->getPoster() . "<br />"; }
Мало того, что набор комментариев прозрачно загружается из базы данных после того, как прокси-сервер был помещен в цикл foreach
, но API, предоставляемый клиентскому коду, сохраняет свою нетронутую структуру без изменений. Мы даже смеем просить что-нибудь лучше? Если вы не безумно жадный, я вряд ли так думаю.
В любом случае, на данный момент вы должны знать, что на самом деле происходит под капотом виртуальных прокси и как максимально использовать функциональность, когда дело доходит до повышения эффективности операций, торгуемых между объектами домена и базовым уровнем персистентности. ,
Заключительные мысли
Хотя это тривиально, особенно если вы достаточно смелы, чтобы использовать его в работе, предыдущий пример в двух словах демонстрирует несколько интересных концепций. Во-первых, виртуальные прокси-серверы не только легко настроить и использовать, но их сложно превзойти, когда речь идет о смешивании различных реализаций во время выполнения, чтобы отложить выполнение дорогостоящих задач, таких как загрузка ленивых больших блоков данных. данные из слоя хранения, или создание тяжеловесных графов объектов.
Во-вторых, это классический пример того, как использование полиморфизма может быть эффективной вакциной для уменьшения распространенных проблем жесткости и хрупкости , от которых страдают многие объектно-ориентированные приложения. Более того, поскольку PHP прост в своей объектной модели и поддерживает Closures , можно создать отличную смесь этих функций и создать прокси-серверы, основная логика которых основана на полезностях замыканий. Если вы хотите решить эту проблему самостоятельно, тогда вы заранее получите мои благословения.
Изображение через imredesiuk / Shutterstock