Статьи

Введение в виртуальные прокси, часть 1

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

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

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

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

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

Прокси не новы для PHP. Doctrine и Zend Framework 2.x используют их, хотя и с разными целями. Однако от имени дидактической причины было бы весьма поучительно реализовать некоторые пользовательские прокси-классы и использовать их для отложенной загрузки нескольких базовых агрегатов из базы данных, таким образом, демонстрируя, как виртуальные прокси-серверы выполняют свою работу под капотом.

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

Итак, теперь давайте двигаться дальше и, наконец, начать работу с виртуальными прокси.

Настройка модели домена

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

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

<?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 setAuthor(AuthorInterface $author);
    public function getAuthor();
}
 <?php
namespace Model;

class Post implements PostInterface
{
    protected $id;
    protected $title;
    protected $content;
    protected $author;

    public function __construct($title, $content, AuthorInterface $author) {
        $this->setTitle($title);
        $this->setContent($content);
        $this->setAuthor($author);
    }

    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 setAuthor(AuthorInterface $author) {
        $this->author = $author;
        return $this;
    }

    public function getAuthor() {
        return $this->author;
    }
}

Поведение класса Post Заметьте, однако, что класс внедряет авторскую реализацию в конструктор, тем самым устанавливая отношения один-к-одному с соответствующим автором.

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

 <?php
namespace Model;

interface AuthorInterface
{
    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 Author implements AuthorInterface
{
    protected $id;
    protected $name;
    protected $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 author has been set already.");
        }

        if (!is_int($id) || $id < 1) {
            throw new InvalidArgumentException(
                "The author 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 name of the author 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 email of the author is invalid.");
        }

        $this->email = $email;
        return $this;
    }

    public function getEmail() {
        return $this->email;
    }
}

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

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

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

Curiosity — действительно довольно зудящая ошибка, поэтому давайте удовлетворим нашу собственную и посмотрим, как создать вышеупомянутый авторский прокси.

Загрузка авторов по запросу через виртуальные прокси

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

 <?php namespace ModelMapper;

interface AuthorMapperInterface
{
    public function fetchById($id);
}
 <?php
namespace ModelMapper;
use LibraryDatabaseDatabaseAdapterInterface,
    ModelAuthor;

class AuthorMapper implements AuthorMapperInterface
{    
    protected $entityTable = "authors";
    
    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 new Author($row["name"], $row["email"]);
    }
}

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

 <?php
namespace ModelProxy;
use ModelMapperAuthorMapperInterface,
    ModelAuthorInterface;

class AuthorProxy implements AuthorInterface
{
    protected $author;
    protected $authorId;
    protected $authorMapper;

    public function __construct($authorId, AuthorMapperInterface $authorMapper) {
        $this->authorId = $authorId;
        $this->authorMapper = $authorMapper;
    }
    
    public function setId($id) {
        $this->authorId = $id;
        return $this;
    }
    
    public function getId() {
        return $this->authorId;
    }

    public function setName($name) {
        $this->loadAuthor();
        $this->author->setName($name);
        return $this;
    }
    
    public function getName() {
        $this->loadAuthor();
        return $this->author->getName();
    }
    
    public function setEmail($email) {
        $this->loadAuthor();
        $this->author->setEmail($email);
        return $this;
    }
    
    public function getEmail() {
        $this->loadAuthor();
        return $this->author->getEmail();
    }
    
    protected function loadAuthor() {
        if ($this->author === null) {
            
            if(!$this->author = $this->authorMapper->fetchById($this->authorId)) {
                throw new UnexpectedValueException(
                    "Unable to fetch the author.");
            }
        }
        
        return $this->author;
    }
}

На первый взгляд класс AuthorProxyAuthor При анализе это показывает логику, которая стоит за виртуальным прокси в двух словах. Метод loadAuthor() Пакет дополнительных методов — это просто оболочки для авторских сеттеров / геттеров.

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

В простой реализации приложение будет выглядеть примерно так:

 <?php
use LibraryLoaderAutoloader,
    LibraryDatabasePdoAdapter,
    ModelMapperAuthorMapper,
    ModelProxyAuthorProxy,
    ModelAuthor,  
    ModelPost;
    
require_once __DIR__ . "/Library/Loader/Autoloader.php";
$autoloader = new Autoloader;
$autoloader->register();

$adapter = new PdoAdapter("mysql:dbname=mydatabase", "dbuser", "dbpassword");

$authorMapper = new AuthorMapper($adapter);

$author = $authorMapper->fetchById(1);

$post = new Post(
    "About Men",
    "Men are born ignorant, not stupid; they are made stupid by education.",
    $author);

echo $post->getTitle() . $post->getContent() . " Quote from: " .
    $post->getAuthor()->getName();

Даже когда преобразователь кавычек был исключен для краткости, поток кода все еще довольно прост для понимания: он извлекает первый авторский объект (ссылка на Бертрана Рассела ) из базы данных, которая вставляется в объект кавычки. Здесь автор был с нетерпением извлечен из хранилища, прежде чем иметь какие-либо шансы его обработать, что, кстати, не является главным грехом.

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

 <?php
$author = new AuthorProxy(1, new AuthorMapper($adapter)); 

$post = new Post(
    "About Men",
    "Men are born ignorant, not stupid; they are made stupid by education.",
    $author);

echo $post->getTitle() . $post->getContent() . " Quote from: " .
    $post->getAuthor()->getName();

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

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

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

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

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

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

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