Легковес / Приспособленец (Flyweight) — шаблон проектирования

26 января 2020
Шаблон предлагает оптимизацию использования объектов, чтобы снизить затраты оперативной памяти и процессора. Оптимизация заключается в вынесении уникальных свойств объектов за пределы объекта, уменьшении количества объектов и построении конструкций, когда клиентский код использует объекты повторно.

Порядок оптимизации:
  1. Допустим, мы используем набор однотипных объектов. В этом наборе проводим изучение свойств объектов и их значений — смотрим, какие свойства содержат одинаковые значения, а какие разные; 
  2. Свойства с разными значениями мы выносим за пределы объектов — такие свойства становятся внешними. А свойства с одинаковыми значениями мы не трогаем, оставляем как есть — они являются внутренними. В итоге получаем несколько групп одинаковых объектов;
  3. Среди одинаковых объектов оставляем один, а остальные удаляем. Тем самым сокращается количество объектов, снижаются расходы оперативной памяти и процессорного времени;
  4. Клиентский код теперь работает с меньшим количеством объектов и при необходимости дополняет объекты внешними свойствами.

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

Пример задачи: в списке статей вывести имя автора.

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



libs.php

Содержит функцию getAuthorFromDB($id), которая имитирует запрос сведений об авторе из базы данных.
<?
/**
 * Получает сведения об авторе из БД.
 * Это тестовая функция, эмулирующая процесс запроса данных из БД.
 * 
 * @param int $id - ID автора
 * @return array|bool - вернёт массив в случае успеха или 'false' в случае ошибки
 */ 
function getAuthorFromDB($id)
{
    $listAuthors = array(
        453 => array(
            'name' => 'Иванов Иван',
            'photo' => '/pics/i_ivanov.jpg',
        ),
        18 => array(
            'name' => 'Пётр Свиридов',
            'photo' => '/pics/p_sviridov.jpg',
        ),
        975 => array(
            'name' => 'Мария Кравцова',
            'photo' => '/pics/mari.jpg',
        ),
        97 => array(
            'name' => 'Семён Лавров',
            'photo' => '/pics/sema.jpg',
        ),
        5 => array(
            'name' => 'Ангелина Маликова',
            'photo' => '/pics/angelina.jpg',
        ),
    );

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

?>

test_before.php

Скрипт до оптимизации, в котором реализован обход статей и получение имени автора отдельным запросом. Запросы в БД здесь виртуальные, чтобы читатель мог запустить пример без развёртывания и наполнения таблиц реальной базы данных. 
<?
//Массив статей
$articles = array(
    array(
        'id' => 1,
        'title' => 'Тестовая статья 1',
        'author' => 18,
        'date' => '2019-12-01',
    ),
    array(
        'id' => 2,
        'title' => 'Тестовая статья 2',
        'author' => 97,
        'date' => '2019-12-02',
    ),
    array(
        'id' => 3,
        'title' => 'Тестовая статья 3',
        'author' => 18,
        'date' => '2019-12-03',
    ),
    array(
        'id' => 4,
        'title' => 'Тестовая статья 4',
        'author' => 5,
        'date' => '2019-12-04',
    ),
    array(
        'id' => 5,
        'title' => 'Тестовая статья 5',
        'author' => 975,
        'date' => '2019-12-05',
    ),
    array(
        'id' => 6,
        'title' => 'Тестовая статья 6',
        'author' => 18,
        'date' => '2019-12-06',
    ),
);

//Подключаем библиотеку функций
require_once(__DIR__.'/libs.php');

//Обходим статьи
$contentList = '';
foreach ($articles  as $item) {
    //Сведения об авторе
    $authorBlock = '';
    $author = getAuthorFromDB($item['author']);
    if (is_array($author)) {
        $authorBlock = '<b>'.htmlspecialchars($author['name']).'</b><br>';
    }

    $contentList .= '
    <div>
        '.$authorBlock.
        htmlspecialchars($item['title']).'
    </div>
    <br>
    ';
}

//Выводим список статей
echo $contentList;

?>

Результат работы скрипта test_before.php
Пётр Свиридов
Тестовая статья 1

Семён Лавров
Тестовая статья 2

Пётр Свиридов
Тестовая статья 3

Ангелина Маликова
Тестовая статья 4

Мария Кравцова
Тестовая статья 5

Пётр Свиридов
Тестовая статья 6

Как можно увидеть по коду, в момент обхода каждой статьи происходит получение сведений об авторе с помощью функции getAuthorFromDB($id), которая находится в файле libs.php. Минус такого способа в том, что в случае вывода объёмного количества статей, например 500 штук, в базу данных будет направлено 500 запросов. 

Для оптимизации получения авторов применим шаблон Легковеса. 

authors.class.php

Представляет класс Authors для работы с авторами статей. Класс является шаблоном Легковеса и состоит из следующих частей:
  • Свойство authors — хранит набор авторов в виде массива. Ключом каждого элемента массива является ID автора. Это сделано, чтобы по ID можно было быстро обратиться к сведениям об авторе;
  • Метод getById($id) — возвращает сведения об авторе по ID. Сначала метод смотрит наличие автора в свойстве authors. Если его там нет, то метод запрашивает данные из базы данных и затем добавляет автора в свойство authors. Тем самым мы сокращаем количество обращений к БД.
<?
//Класс авторов статей
class Authors
{
    /**
     * Массив с авторами
     * @var array
     */
    private static $authors = array();

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

        if (isset(self::$authors[$id])) {
            return self::$authors[$id];
        } else {
            //Подключаем библиотеку функций
            require_once(__DIR__.'/libs.php');

            //Получаем запись автора из БД
            $author = getAuthorFromDB($id);
            if (is_array($author)) {
                self::$authors[$id] = $author;
                return self::$authors[$id];
            } else {
                return false;
            }
        }
    }
}

?>

test_after.php 

В скрипте всё также содержится обход статей, только сведения об авторе мы уже получаем с помощью метода getById($id) класса Authors. Если в статьях будут попадаться одинаковые авторы, то класс не станет делать повторные запросы в базу данных, а возьмёт данные из свойства authors. Таким образом, несколько статей будут обращаться к одному и тому же элементу автора и участку памяти. Этого мы и стремились, применив шаблон Легковеса и оптимизировав использование ресурсов сервера.
<?
//Массив статей
$articles = array(
    array(
        'id' => 1,
        'title' => 'Тестовая статья 1',
        'author' => 18,
        'date' => '2019-12-01',
    ),
    array(
        'id' => 2,
        'title' => 'Тестовая статья 2',
        'author' => 97,
        'date' => '2019-12-02',
    ),
    array(
        'id' => 3,
        'title' => 'Тестовая статья 3',
        'author' => 18,
        'date' => '2019-12-03',
    ),
    array(
        'id' => 4,
        'title' => 'Тестовая статья 4',
        'author' => 5,
        'date' => '2019-12-04',
    ),
    array(
        'id' => 5,
        'title' => 'Тестовая статья 5',
        'author' => 975,
        'date' => '2019-12-05',
    ),
    array(
        'id' => 6,
        'title' => 'Тестовая статья 6',
        'author' => 18,
        'date' => '2019-12-06',
    ),
);

//Подключаем класс для работы с авторами статей
require_once(__DIR__.'/authors.class.php');

//Обходим статьи
$contentList = '';
foreach ($articles  as $item) {
    //Сведения об авторе
    $authorBlock = '';
    $author = Authors::getById($item['author']);
    if (is_array($author)) {
        $authorBlock = '<b>'.htmlspecialchars($author['name']).'</b><br>';
    }

    $contentList .= '
    <div>
        '.$authorBlock.
        htmlspecialchars($item['title']).'
    </div>
    <br>
    ';
}

//Выводим список статей
echo $contentList;

?>

Результат работы скрипта test_after.php
Пётр Свиридов
Тестовая статья 1

Семён Лавров
Тестовая статья 2

Пётр Свиридов
Тестовая статья 3

Ангелина Маликова
Тестовая статья 4

Мария Кравцова
Тестовая статья 5

Пётр Свиридов
Тестовая статья 6