НЕ МОЛЧИ!!!    Сделай что-нибудь,
чтобы остановить войну России в Украине.
...бойтесь людей равнодушных - именно с их молчаливого согласия происходят все самые ужасные преступления на свете.   ("Репортаж с петлёй на шее")

Шаблон проектирования Flyweight(Легковес)


Рассматривается структурный шаблон проектирования Flyweight (Легковес), его назначение, особенности применения и пример реализации на PHP 8.

Содержание

  • Применение
  • Структура шаблона
  • Реализация
  • Преимущества и недостатки
  • Итоги

Применение

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

Пример использования шаблона

Проще всего будет пояснить использование шаблона на конкретном примере.
Предположим, что мы создаём приложение для большой транспортной компании. Стоит задача отслеживать передвижение транспортных средств предприятия. Допустим, каждое транспортное средство должно иметь следующие характеристики: производитель, модель, аватар, номерной знак, геокоординаты и время получения координат.

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

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

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

Надеюсь, суть шаблона Flyweight ясна.

Можно сформулировать её ещё короче: общую неизменяемую часть состояния храним в легковесе и используем повторно, а зависящую от контекста часть объект получает от клиента.

Остаётся добавить, что при использовании большого количества легковесов обычно применяется шаблон проектирования Фабрика. Он отвечает за получение уже существующего объекта или создание нового, если требуемый объект ещё не был создан.

Теперь приступим к реализации.


Структура шаблона


В нашем примере участвуют три основных класса.

Car — легковес. Хранит внутреннее состояние автомобиля, общее для всех случаев его использования.
CarFactory — фабрика легковесов. Хранит созданные объекты Car и возвращает требуемый объект либо создаёт новый, если его нет у фабрики.
CarState — внешнее состояние автомобиля. Содержит данные, относящиеся к конкретному автомобилю в конкретный момент времени.

Они взаимодействуют следующим образом. Клиент получает легковес у фабрики и передаёт ему внешнее состояние. Упрощённо взаимодействие объектов можно представить так.

+----------------+

                    |     Client     |
                    +----------------+
                            |
                            | getCar()
                            v
                    +----------------+
                    |   CarFactory   |
                    +----------------+
                            |
                            | returns
                            v
                    +----------------+
                    |      Car       |
                    |----------------|
                    | manufacturer   |
                    | model          |
                    | avatar         |
                    +----------------+
                            ^
                            |
                     getCurrentState()
                            |
                    +----------------+
                    |    CarState    |
                    |----------------|
                    | number         |
                    | latitude       |
                    | longitude      |
                    | time           |
                    +----------------+

UML-диаграмма классов

На диаграмме хорошо видно разделение ответственности. CarFactory управляет легковесами Car, а CarState содержит внешнее состояние, которое клиент передаёт легковесу.

UML диаграмма примера

Реализация


Начнём с класса Car.
Он содержит только внутреннее состояние — производителя, модель автомобиля и аватар.

<?php

declare(strict_types=1);

final class Car
{
    public function __construct(
        private readonly string $manufacturer,
        private readonly string $model,
        private readonly string $avatar
    ) {
    }

    public function getCurrentState(CarState $state): array
    {
        return [
            'manufacturer' => $this->manufacturer,
            'model' => $this->model,
            'avatar' => $this->avatar,
            'number' => $state->getNumber(),
            'latitude' => $state->getLatitude(),
            'longitude' => $state->getLongitude(),
            'time' => $state->getTime()->format('d.m.Y H:i:s'),
        ];
    }
}


Обратите внимание, что Car не хранит номер автомобиля, его координаты и время получения координат. Эти данные относятся к внешнему состоянию и передаются методу getCurrentState() при каждом его использовании.
Свойства $manufacturer, $model и $avatar объявлены как readonly. После создания объекта они не могут быть изменены, т.е. соответствуют роли внутреннего состояния легковеса.

Теперь создадим класс CarState, который будет хранить внешнее состояние.

<?php

declare(strict_types=1);

final class CarState
{
    public function __construct(
        private readonly string $number,
        private readonly float $latitude,
        private readonly float $longitude,
        private readonly DateTimeImmutable $time
    ) {
    }

    public function getNumber(): string
    {
        return $this->number;
    }

    public function getLatitude(): float
    {
        return $this->latitude;
    }

    public function getLongitude(): float
    {
        return $this->longitude;
    }

    public function getTime(): DateTimeImmutable
    {
        return $this->time;
    }
}

Теперь нам нужна фабрика.
Именно она будет управлять созданными легковесами.

<?php

declare(strict_types=1);

final class CarFactory
{
    /**
     * @var array<string, Car>
     */
    private array $cars = [];

    public function getCar(
        string $manufacturer,
        string $model,
        string $avatar
    ): Car {
        $key = $manufacturer . ':' . $model;

        if (!isset($this->cars[$key])) {
            $this->cars[$key] = new Car(
                $manufacturer,
                $model,
                $avatar
            );
        }
        return $this->cars[$key];
    }
}

В простейшем случае ключом для поиска легковеса является сочетание производителя и модели автомобиля.

При первом обращении к фабрике объект Car создаётся, при повторном обращении с теми же параметрами возвращается существующий.

$car = $factory->getCar(
    'Volkswagen',
    'Crafter',
    '/images/cars/crafter.png'
);

Теперь давайте рассмотрим, как с фабрикой работает клиент.

<?php

declare(strict_types=1);

$factory = new CarFactory();

$car = $factory->getCar(
    'Volkswagen',
    'Crafter',
    '/images/cars/crafter.png'
);

$state = new CarState(
    number: 'А123ВС',
    latitude: 50.462099,
    longitude: 30.433526,
    time: new DateTimeImmutable()
);

$result = $car->getCurrentState($state);

print_r($result);

Результатом работы будет массив примерно следующего вида:

Array
(
    [manufacturer] => Volkswagen
    [model] => Crafter
    [avatar] => /images/cars/crafter.png
    [number] => А123ВС
    [latitude] => 50.462099
    [longitude] => 30.433526
    [time] => 08.08.2026 12:00:00
)

При этом самое интересное происходит не при формировании массива, а при повторном использовании легковеса.
Предположим, что у компании имеется большое количество автомобилей модели Crafter:

$car1 = $factory->getCar(
    'Volkswagen',
    'Crafter',
    '/images/cars/crafter.png'
);

$car2 = $factory->getCar(
    'Volkswagen',
    'Crafter',
    '/images/cars/crafter.png'
);


Смотрите, переменные $car1 и $car2 будут ссылаться на один и тот же объект:

var_dump($car1 === $car2);


Результат: bool(true)


При этом состояние автомобилей может быть разным:

$state1 = new CarState(
    number: 'А123ВС',
    latitude: 50.462099,
    longitude: 30.433526,
    time: new DateTimeImmutable()
);

$state2 = new CarState(
    number: 'А456ВС',
    latitude: 50.450001,
    longitude: 30.524600,
    time: new DateTimeImmutable()
);

print_r($car1->getCurrentState($state1));
print_r($car2->getCurrentState($state2));

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

Именно в этом и заключается основная идея Flyweight.


Преимущества и недостатки

Основное преимущество шаблона Flyweight — уменьшение количества объектов и, как следствие, сокращение объёма памяти, необходимого приложению.

Особенно заметным эффект будет при выполнении следующих условий:

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

Кроме экономии памяти, повторное использование объектов позволяет явно разделить общие данные и данные, зависящие от контекста.

Однако у шаблона есть и недостатки.

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

Клиенту также необходимо передавать внешнее состояние при каждом обращении к легковесу.

Кроме того, применение Flyweight не всегда оправдано. Если объектов немного или они почти не содержат одинаковых данных, усложнение архитектуры может не дать заметного выигрыша.

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

Итоги


Итак, мы рассмотрели структурный шаблон проектирования Flyweight (Легковес).
Основная идея шаблона заключается в экономии ресурсов сервера за счёт повторного использования объектов с одинаковым внутренним состоянием. Общая неизменяемая часть состояния хранится в легковесе, а зависящую от контекста часть объект получает от клиента.

В нашем примере фабрика CarFactory создаёт объекты Car и повторно использует их для автомобилей одной модели. Объект CarState содержит данные, которые относятся к конкретному автомобилю в конкретный момент времени.

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

На этом пока всё.

Успехов в разработке!

Перейти к списку шаблонов проектирования.