Заместитель (Proxy) — шаблон проектирования

27 января 2020
Шаблон предлагает класс-заместитель для перехвата вызываемых методов исходного класса. Паттерн используется для действий, которые нужно выполнить до или после вызова методов исходного класса — например, для логирования, кеширования, проверки прав доступа и других действий. А так как класс-заместитель повторяет публичные свойства и методы исходного класса, то клиентский код не требует внесения больших изменений. По факту, Заместитель выступает в роли прокси-агента между клиентским кодом и исходным классом. 

Паттерн хоть и похож на шаблон Адаптера и Декоратора, но всё же имеет отличия. Например, в сравнении с Адаптером, Заместитель обязан повторять внешний интерфейс исходного класса. А если сравнивать с Декоратором, то Заместитель не изменяет поведенческую логику исходного класса.

Пример задачи: сделать кэширование статей.

Код реализации паттерна на языке PHP (требуется включение модуля Memcached). Скачать исходники (ZIP, 3 Кб)



libs.php

Содержит функции, которые имитируют выборку статей из базы данных:
  • getArticlesFromDB() – возвращает список статей;
  • getArticleFromDB($id) – возвращает статью по её ID. 
<?
//Глобальная переменная для имитации хранилища статей
GLOBAL $tableArticles;
$tableArticles = array(
    1 => array(
            'title' => 'Тестовая статья 1',
            'description' => 'Тестовый текст с описанием статьи 1',
    ),
    2 => array(
            'title' => 'Тестовая статья 2',
            'description' => 'Тестовый текст с описанием статьи 2',
    ),
    3 => array(
            'title' => 'Тестовая статья 3',
            'description' => 'Тестовый текст с описанием статьи 3',
    ),
    4 => array(
            'title' => 'Тестовая статья 4',
            'description' => 'Тестовый текст с описанием статьи 4',
    ),
    5 => array(
            'title' => 'Тестовая статья 5',
            'description' => 'Тестовый текст с описанием статьи 5',
    )
);

/**
 * Получает статьи из базы данных.
 * Это виртуальная функция, имитирующая запрос в БД
 * 
 * @return array - вернёт массив статей
 */ 
function getArticlesFromDB()
{
    GLOBAL $tableArticles;
    return $tableArticles;
}

/**
 * Возвращает статью по её идентификатору (ID).
 * Это виртуальная функция, имитирующая запрос в БД
 * 
 * @param int $id - ID статьи
 * @return array|bool - вернёт массив в случае успеха или 'false' в случае, если статья не будет найдена
 */ 
function getArticleFromDB($id)
{
    GLOBAL $tableArticles;
    $id = (int)$id;

    if (isset($tableArticles[$id])) {
        return $tableArticles[$id];
    } else {
        return false;
    }
}

?>

articles.class.php

Содержит класс Articles для работы со статьями. Класс состоит из следующих методов:
  • getList() — возвращает список статей, используя функцию getArticlesFromDB() для выборки статей из БД;
  • getById($id) — возвращает статью по её идентификатору (ID), используя функцию getArticleFromDB($id) для выборки статьи из БД.
<?
//Подключаем библиотеку дополнительных функций
require_once(__DIR__.'/libs.php');

//Класс статей
class Articles
{
    /**
     * Возвращает список статей
     * @return array
     */
    public function getList()
    {
        //Получаем список статей из БД (функция имитирует выборку из базы данных)
        $items = getArticlesFromDB();

        return $items;
    }

    /**
     * Возвращает статью по её идентификатору (ID).
     * 
     * @param int $id - ID статьи
     * @return array|bool - вернёт массив в случае успеха или 'false' в случае, если статья не будет найдена
     */
    public function getById($id)
    {
        //Приведение типов
        $id = (int)$id;

        $item = getArticleFromDB($id);
        return $item;
    }
}

?>

test_simple.php

Содержит тестовый скрипт, в котором создаётся объект класса Articles, затем через него происходит получение сначала списка статей, а потом одной статьи с ID = 3. В конце полученные данные выводятся на экран.
<?
//Подключаем класс для работы со статьями
require_once(__DIR__.'/articles.class.php');

$articles = new Articles();

$items = $articles->getList();
$item = $articles->getById(3);

echo '<b>Список статей:</b><br>';
echo '<pre>'.print_r($items, true).'</pre><br>';

echo '<b>Статья с ID = 3:</b><br>';
echo '<pre>'.print_r($item, true).'</pre>';

?>

Результат работы скрипта test_simple.php
Список статей:

Array
(
    [1] => Array
        (
            [title] => Тестовая статья 1
            [description] => Тестовый текст с описанием статьи 1
        )

    [2] => Array
        (
            [title] => Тестовая статья 2
            [description] => Тестовый текст с описанием статьи 2
        )

    [3] => Array
        (
            [title] => Тестовая статья 3
            [description] => Тестовый текст с описанием статьи 3
        )

    [4] => Array
        (
            [title] => Тестовая статья 4
            [description] => Тестовый текст с описанием статьи 4
        )

    [5] => Array
        (
            [title] => Тестовая статья 5
            [description] => Тестовый текст с описанием статьи 5
        )

)


Статья с ID = 3:

Array
(
    [title] => Тестовая статья 3
    [description] => Тестовый текст с описанием статьи 3
)

Теперь нужно решить задачу кэширования статей, в этом нам поможет модуль Memcached и шаблон Заместитель. Мы не будет менять класс Articles, а создадим для него замещающий класс ArticlesCached, который будет выполнять всю работу, связанную с кэшированием.

Важный момент! Чтобы клиентский код не пришлось сильно переделывать, класс ArticlesCached должен содержать все публичные методы класса Articles

articles_cached.class.php

Содержит класс ArticlesCached для работы со статьями и их кэшированием. Класс состоит из следующих элементов:
  • articles — свойство, объект класса Articles, используется для получения статей;
  • memcached — свойство, объект кэша Memcached;
  • cacheTime — свойство, время, в течение которого данные из кэша считаются актуальными;
  • __construct($cacheTime = 3600) — конструктор, в котором создаётся объект класса Articles и затем помещается в свойство articles;
  • getList() — метод возвращает список статей.
    Метод запрашивает статьи из кэша и если их там нету или данные в кэше уже устарели, то получает статьи через свойство articles (объект класса Articles), вызывая его метод getList(). Затем полученные актуальные данные сохраняются в кэш;
  • getById($id) — метод возвращает статью по её идентификатору (ID).
    Аналогично, метод запрашивает статью из кэша и в случае её отсутствия или когда данные кэша уже неактуальны, то получает статью через свойство articles (объект класса Articles), вызывая его метод getById($id). Затем полученные актуальные данные сохраняются в кэш.

Как можно увидеть, класс ArticlesCached не заменяет функционал класса Articles. Напротив, Articles продолжает использоваться, только теперь между клиентским кодом и классом статей Acrticles появилась прослойка в виде класса ArticlesCached и клиентский код теперь станет работать с прослойкой, а не напрямую с классом Articles.
 
<?
//Класс статей Articles
require_once(__DIR__.'/articles.class.php');

/**
 * Класс статей с поддержкой кэширования.
 * В качестве кэша используется система Memcached 
 */
class ArticlesCached
{
    /**
     * Объект исходного класса Articles для работы со статьями
     * @var Articles
     */
    private $articles;

    /**
     * Объект кэша Memcached
     * @var Memcached
     */
    private $memcached;

    /**
     * Вмемя жизни кэша
     * @var int
     */
    private $cacheTime;

    /**
     * Конструктор объекта
     * @param int $cacheTime - время жизни кэша в секундах. По умолчанию время жизни составляет 1 час (3600 секунд)
     */
    public function __construct($cacheTime = 3600)
    {
        if (class_exists('Memcached') == false) {
            throw new Exception('Для работы с классом ArticlesCached нужно включить поддержку системы кэширования Memcached');
        }

        //Создаём объект Memcached и соединяемся с сервером
        $this->memcached = new Memcached();
        if ($this->memcached->addServer('localhost', 11211) == false) {
            throw new Exception('Не удалось соедениться с сервером Memcached');
        }

        $this->articles = new Articles();
        $this->cacheTime = (int)$cacheTime;
    }

    /**
     * Возвращает список статей
     * @return array
     */
    public function getList()
    {
        //Сначала запрашиваем статьи из кэша. Если их там нету, то получаем статьи напрямую из объекта класса Articles
        $items = $this->memcached->get('articles'); 
        if (is_array($items) == false) {
            $items = $this->articles->getList();
            $this->memcached->set('articles', $items, $this->cacheTime); 
        }

        return $items;
    }

    /**
     * Возвращает статью по её идентификатору (ID).
     * 
     * @param int $id - ID статьи
     * @return array|bool - вернёт массив в случае успеха или 'false' в случае, если статья не будет найдена
     */
    public function getById($id)
    {
        //Приведение типов
        $id = (int)$id;

        //Ключ статьи в кэше
        $keyArticle = 'article_'.$id;

        //Сначала запрашиваем статью из кэша. Если её там нету, то получаем статью напрямую из объекта класса Articles
        $item = $this->memcached->get($keyArticle); 
        if (is_array($item) == false) {
            $item = $this->articles->getById($id);
            if (is_array($item) == true) {
                $this->memcached->set($keyArticle, $item, $this->cacheTime); 
            }
        }

        return $item;
    }
}

?>

test_cached.php

Скрипт содержит такой же код, как в скрипте test_simple.php, только класс Articles мы заменили на ArticlesCached и теперь статьи кэшируются.
<?
//Подключаем класс для работы со статьями с поддержкой кэширования
require_once(__DIR__.'/articles_cached.class.php');

$articles = new ArticlesCached();

$items = $articles->getList();
$item = $articles->getById(3);

echo '<b>Список статей:</b><br>';
echo '<pre>'.print_r($items, true).'</pre><br>';

echo '<b>Статья с ID = 3:</b><br>';
echo '<pre>'.print_r($item, true).'</pre>';

?>

Результат работы скрипта test_cached.php
Список статей:

Array
(
    [1] => Array
        (
            [title] => Тестовая статья 1
            [description] => Тестовый текст с описанием статьи 1
        )

    [2] => Array
        (
            [title] => Тестовая статья 2
            [description] => Тестовый текст с описанием статьи 2
        )

    [3] => Array
        (
            [title] => Тестовая статья 3
            [description] => Тестовый текст с описанием статьи 3
        )

    [4] => Array
        (
            [title] => Тестовая статья 4
            [description] => Тестовый текст с описанием статьи 4
        )

    [5] => Array
        (
            [title] => Тестовая статья 5
            [description] => Тестовый текст с описанием статьи 5
        )

)


Статья с ID = 3:

Array
(
    [title] => Тестовая статья 3
    [description] => Тестовый текст с описанием статьи 3
)