Перейти к содержанию

Flyweight (Легковес)

Категория: структурный паттерн.

Проблема

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

Решение

Состояние разделяется на две части:

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

Фабрика легковесов находит уже созданный объект с нужным внутренним состоянием или создаёт его один раз. Множество контекстов затем ссылаются на один экземпляр.

Структура

  • Flyweight - хранит разделяемое внутреннее состояние и выполняет операцию с переданным внешним состоянием.
  • Context - хранит уникальное внешнее состояние и ссылку на легковес.
  • FlyweightFactory - кеширует и переиспользует легковесы по ключу внутреннего состояния.
  • Client - создаёт контексты и запрашивает легковесы у фабрики.

Когда применять

Только когда измерения показывают, что одновременно выполняются условия:

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

Плюсы

  • Существенно снижает расход памяти при большом числе однотипных объектов.
  • Централизует создание и повторное использование общего состояния.

Минусы

  • Усложняет модель разделением состояния и передачей контекста в методы.
  • Может увеличить вычислительные затраты на поиск легковеса и восстановление внешнего состояния.
  • Изменяемый легковес опасен: изменение затронет сразу все использующие его контексты.

Отличие от кеша и Singleton

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

Пример в .NET

  • Интернирование строк (string.Intern) позволяет одинаковым строковым значениям разделять один экземпляр, хотя ручное интернирование следует применять осторожно из-за времени жизни intern pool.
  • Пулы и кеши метаданных могут использовать ту же идею разделения тяжёлого неизменяемого состояния, но не каждый пул является Flyweight.

Источник для сверки структуры: Refactoring.Guru - Flyweight.

Пример реализации на C#

Flyweight.cs
using System;
using System.Collections.Generic;

namespace DesignPatterns.Structural.Flyweight
{
    // Легковес хранит только разделяемое внутреннее состояние и неизменяем.
    public sealed class TreeType
    {
        public TreeType(string name, string color, string texture)
        {
            Name = name;
            Color = color;
            Texture = texture;
        }

        public string Name { get; }
        public string Color { get; }
        public string Texture { get; }

        public string Draw(int x, int y) =>
            $"{Name} ({Color}, {Texture}) в точке ({x}, {y})";
    }

    public sealed class TreeTypeFactory
    {
        private readonly Dictionary<(string Name, string Color, string Texture), TreeType>
            _types = new();

        public TreeType Get(string name, string color, string texture)
        {
            var key = (name, color, texture);
            if (!_types.TryGetValue(key, out TreeType? type))
            {
                type = new TreeType(name, color, texture);
                _types.Add(key, type);
            }

            return type;
        }

        public int Count => _types.Count;
    }

    // Контекст хранит уникальное внешнее состояние и ссылку на общий легковес.
    public sealed class Tree
    {
        public Tree(int x, int y, TreeType type)
        {
            X = x;
            Y = y;
            Type = type;
        }

        public int X { get; }
        public int Y { get; }
        public TreeType Type { get; }
        public string Draw() => Type.Draw(X, Y);
    }

    public static class Demo
    {
        public static void Run()
        {
            var factory = new TreeTypeFactory();
            var forest = new List<Tree>
            {
                new(10, 20, factory.Get("Берёза", "белая", "birch.png")),
                new(30, 40, factory.Get("Берёза", "белая", "birch.png")),
                new(50, 60, factory.Get("Сосна", "зелёная", "pine.png"))
            };

            foreach (Tree tree in forest)
                Console.WriteLine(tree.Draw());

            Console.WriteLine($"Деревьев: {forest.Count}, общих типов: {factory.Count}");
            Console.WriteLine(ReferenceEquals(forest[0].Type, forest[1].Type)); // True
        }
    }
}

Открыть Flyweight.cs отдельно Скачать Flyweight.cs