Весняний РОЗПРОДАЖ

Легковаговик

Також відомий як: Пристосуванець, Кеш, Flyweight

Суть патерна

Легковаговик — це структурний патерн проектування, що дає змогу вмістити більшу кількість об’єктів у відведеній оперативній пам’яті. Легковаговик заощаджує пам’ять, розподіляючи спільний стан об’єктів між собою, замість зберігання однакових даних у кожному об’єкті.

Патерн Легковаговик

Проблема

На дозвіллі ви вирішили написати невелику гру, в якій гравці переміщуються по карті та стріляють один в одного. Фішкою гри повинна була стати реалістична система частинок. Кулі, снаряди, уламки від вибухів — все це повинно реалістично літати та гарно виглядати.

Гра відмінно працювала на вашому потужному комп’ютері, проте ваш друг повідомив, що гра починає гальмувати й вилітає через кілька хвилин після запуску. Передивившись логи, ви виявили, що гра вилітає через нестачу оперативної пам’яті. У вашого друга комп’ютер значно менше «прокачаний», тому проблема в нього й проявляється так швидко.

Дійсно, кожна частинка у грі представлена власним об’єктом, що має безліч даних. У певний момент, коли побоїще на екрані досягає кульмінації, оперативна пам’ять комп’ютера вже не може вмістити нові об’єкти частинок, і програма «вилітає».

Проблема патерна Легковаговик

Рішення

Якщо уважно подивитися на клас частинок, то можна помітити, що колір і спрайт займають найбільше пам’яті. Більше того, ці поля зберігаються в кожному об’єкті, хоча фактично їхні значення є однаковими для більшості частинок.

Рішення патерна Легковаговик

Інший стан об’єктів — координати, вектор руху й швидкість відрізняються для всіх частинок. Таким чином, ці поля можна розглядати як контекст, у якому використовується частинка, а колір і спрайт — це дані, що не змінюються в часі.

Незмінні дані об’єкта прийнято називати «внутрішнім станом». Всі інші дані — це «зовнішній стан».

Патерн Легковаговик пропонує не зберігати зовнішній стан у класі, а передавати його до тих чи інших методів через параметри. Таким чином, одні і ті самі об’єкти можна буде повторно використовувати в різних контекстах. Головна ж перевага в тому, що тепер знадобиться набагато менше об’єктів, адже вони тепер відрізнятимуться тільки внутрішнім станом, а він не має так багато варіацій.

Рішення патерна Легковаговик

У нашому прикладі з частинками достатньо буде залишити лише три об’єкти, що відрізняються спрайтами і кольором — для куль, снарядів та уламків. Нескладно здогадатися, що такі полегшені об’єкти називають легковаговиками .

Сховище зовнішнього стану

Але куди переїде зовнішній стан? Адже хтось повинен його зберігати. Найчастіше його переміщують до контейнера, який керував об’єктами до застосування патерна.

В нашому випадку це був головний клас гри. Ви могли б додати до його класу поля-масиви для зберігання координат, векторів і швидкостей частинок. Крім цього, потрібен буде ще один масив для зберігання посилань на об’єкти-легковаговики, що відповідають тій чи іншій частинці.

Рішення патерна Легковаговик

Більш елегантним рішенням було б створити додатковий клас-контекст, який пов’язував би зовнішній стан з тим чи іншим легковаговиком. Це дозволить обійтися тільки одним полем-масивом у класі контейнера.

«Але стривайте, нам буде потрібно стільки ж цих об’єктів, скільки було на самому початку!» — скажете ви і будете праві! Але річ у тім, що об’єкти-контексти займають набагато менше місця, ніж початкові. Адже найважчі поля залишилися всередині легковаговика (вибачте за каламбур), і зараз ми будемо посилатися на ці об’єкти з контекстів, замість того, щоб повторно зберігати стан, що дублюється.

Незмінність Легковаговиків

Оскільки об’єкти легковаговиків будуть використані в різних контекстах, ви повинні бути впевненими в тому, що їхній стан неможливо змінити після створення. Весь внутрішній стан легковаговик повинен отримувати через параметри конструктора. Він не повинен мати сеттерів і публічних полів.

Фабрика Легковаговиків

Для зручності роботи з легковаговиками і контекстами можна створити фабричний метод, що приймає в параметрах увесь внутрішній (іноді й зовнішній) стан бажаного об’єкта.

Найбільша користь цього методу в тому, щоб знаходити вже створених легковаговиків з таким самим внутрішнім станом, як потрібно. Якщо легковаговик знаходиться, його можна повторно використовувати. Якщо немає — просто створюємо новий.

Зазвичай цей метод додають до контейнера легковаговиків або створюють окремий клас-фабрику. Його навіть можна зробити статичним і розмістити в класі легковаговиків.

Структура

Патерн Легковаговик (Пристосуванець)Патерн Легковаговик (Пристосуванець)
  1. Ви завжди повинні пам’ятати про те, що легковаговик застосовується в програмі, яка має величезну кількість однакових об’єктів. Цих об’єктів повинно бути так багато, щоб вони не вміщалися в доступній оперативній пам’яті без додаткових хитрощів. Патерн розділяє дані цих об’єктів на дві частини — легковаговики та контексти.

  2. Легковаговик містить стан, який повторювався в багатьох первинних об’єктах. Один і той самий легковаговик може використовуватись у зв’язці з безліччю контекстів. Стан, що зберігається тут, називається внутрішнім, а той, який він отримує ззовні, — зовнішнім.

  3. Контекст містить «зовнішню» частину стану, унікальну для кожного об’єкта. Контекст пов’язаний з одним з об’єктів-легковаговиків, що зберігають стан, який залишився.

  4. Поведінку оригінального об’єкта найчастіше залишають у легковаговику, передаючи значення контексту через параметри методів. Тим не менше, поведінку можна розмістити й в контексті, використовуючи легковаговик як об’єкт даних.

  5. Клієнт обчислює або зберігає контекст, тобто зовнішній стан легковаговиків. Для клієнта легковаговики виглядають як шаблонні об’єкти, які можна налаштувати під час використання, передавши контекст через параметри.

  6. Фабрика легковаговиків керує створенням і повторним використанням легковаговиків. Фабрика отримує запити, в яких зазначено бажаний стан легковаговика. Якщо легковаговик з таким станом вже створений, фабрика відразу його повертає, а якщо ні — створює новий об’єкт.

Псевдокод

У цьому прикладі Легковаговик допомагає заощадити оперативну пам’ять при відображенні на екрані мільйонів об’єктів-дерев.

Структура класів прикладу патерна Легковаговик

Легковаговик виділяє повторювану частину стану з основного класу Tree і розміщує його в додатковому класі TreeType.

Тепер, замість зберігання повторюваних даних в усіх об’єктах, окремі дерева будуть посилатися на кілька спільних об’єктів, що зберігають ці дані. Клієнт працює з деревами через фабрику дерев, яка приховує від нього складність кешування спільних даних дерев.

Таким чином, програма буде використовувати набагато менше оперативної пам’яті, що дозволить намалювати на екрані більше дерев, використовуючи те ж саме «залізо».

// Цей клас-легковаговик містить лише частину полів, які
// описують дерева. На відміну, наприклад, від координат, ці
// поля не є унікальними для кожного дерева, оскільки декілька
// дерев можуть мати такий самий колір чи текстуру. Тому ми
// переносимо повторювані дані до одного єдиного об'єкта й
// посилаємося на нього з множини окремих дерев.
class TreeType is
    field name
    field color
    field texture
    constructor TreeType(name, color, texture) { ... }
    method draw(canvas, x, y) is
        // 1. Створити зображення даного типу, кольору й
        // текстури.
        // 2. Відобразити його на полотні в позиції X, Y.

// Фабрика легковаговиків вирішує, коли потрібно створити нового
// легковаговика, а коли можна обійтися існуючим.
class TreeFactory is
    static field treeTypes: collection of tree types
    static method getTreeType(name, color, texture) is
        type = treeTypes.find(name, color, texture)
        if (type == null)
            type = new TreeType(name, color, texture)
            treeTypes.add(type)
        return type

// Контекстний об'єкт, з якого ми виділили легковаговик
// TreeType. У програмі можуть бути тисячі об'єктів Tree,
// оскільки накладні витрати на їхнє зберігання зовсім
// невеликі — в пам'яті треба зберігати лише три цілих числа
// (дві координати й посилання).
class Tree is
    field x,y
    field type: TreeType
    constructor Tree(x, y, type) { ... }
    method draw(canvas) is
        type.draw(canvas, this.x, this.y)

// Класи Tree і Forest є клієнтами Легковаговика. За умови, що
// надалі вам не потрібно розширювати клас дерев, їх можна злити
// докупи.
class Forest is
    field trees: collection of Trees

    method plantTree(x, y, name, color, texture) is
        type = TreeFactory.getTreeType(name, color, texture)
        tree = new Tree(x, y, type)
        trees.add(tree)

    method draw(canvas) is
        foreach (tree in trees) do
            tree.draw(canvas)

Застосування

Якщо не вистачає оперативної пам’яті для підтримки всіх потрібних об’єктів.

Ефективність патерна Легковаговик багато в чому залежить від того, як і де він використовується. Застосовуйте цей патерн у випадках, коли виконано всі перераховані умови:

  • у програмі використовується велика кількість об’єктів;
  • через це високі витрати оперативної пам’яті;
  • більшу частину стану об’єктів можна винести за межі їхніх класів;
  • великі групи об’єктів можна замінити невеликою кількістю об’єктів, що розділяються, оскільки зовнішній стан винесено.

Кроки реалізації

  1. Розділіть поля класу, який стане легковаговиком, на дві частини:

    • внутрішній стан: значення цих полів однакові для великої кількості об’єктів.
    • зовнішній стан (контекст): значення полів унікальні для кожного об’єкта.
  2. Залишіть поля внутрішнього стану в класі, але переконайтеся, що їхні значення неможливо змінити. Ці поля повинні ініціалізуватись тільки через конструктор.

  3. Перетворіть поля зовнішнього стану на параметри методів, у яких ці поля використовувалися. Потім видаліть поля з класу.

  4. Створіть фабрику, яка буде кешувати та повторно віддавати вже створені об’єкти. Клієнт повинен отримувати легковаговика з певним внутрішнім станом саме з цієї фабрики, а не створювати його безпосередньо.

  5. Клієнт повинен зберігати або обчислювати значення зовнішнього стану (контекст) і передавати його до методів об’єкта легковаговика.

Переваги та недоліки

  • Заощаджує оперативну пам’ять.
  • Витрачає процесорний час на пошук/обчислення контексту.
  • Ускладнює код програми внаслідок введення безлічі додаткових класів.

Відносини з іншими патернами

  • Компонувальник часто поєднують з Легковаговиком, щоб реалізувати спільні гілки дерева та заощадити при цьому пам’ять.

  • Легковаговик показує, як створювати багато дрібних об’єктів, а Фасад показує, як створити один об’єкт, який відображає цілу підсистему.

  • Патерн Легковаговик може нагадувати Одинака, якщо для конкретного завдання ви змогли зменшити кількість об’єктів до одного. Але пам’ятайте, що між патернами є дві суттєві відмінності:

    1. На відміну від Одинака, ви можете мати безліч об’єктів-легковаговиків.
    2. Об’єкти-легковаговики повинні бути незмінними, тоді як об’єкт-одинак допускає зміну свого стану.

Приклади реалізації патерна

Легковаговик на C# Легковаговик на C++ Легковаговик на Go Легковаговик на Java Легковаговик на PHP Легковаговик на Python Легковаговик на Ruby Легковаговик на Rust Легковаговик на Swift Легковаговик на TypeScript