PHP: Легковес

Flyweight Легковес Flyweight

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

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

Подробней о Легковесе

Особенности паттерна на PHP

Сложность:

Популярность:

Применимость: Весь смысл использования Легковеса — в экономии памяти. Поэтому, если в приложении нет такой проблемы, то вы вряд ли найдёте там примеры Легковеса.

Паттерн особенно редко актуален для PHP-приложений, из-за самой природы языка. Скрипты почти всегда работают с данными приложения частями, никогда не загружая все данные в память одновременно.

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

Пример: Структура паттерна

Этот пример показывает структуру паттерна Легковес, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом. После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.

FlyweightStructural.php: Пример структуры паттерна

<?php

namespace RefactoringGuru\Flyweight\Structural;

/**
 * Легковес хранит общую часть состояния (также называемую внутренним
 * состоянием), которая принадлежит нескольким реальным бизнес-объектам.
 * Легковес принимает  оставшуюся часть состояния (внешнее состояние, уникальное
 * для каждого объекта)  через его параметры метода.
 */
class Flyweight
{
    private $sharedState;

    public function __construct($sharedState)
    {
        $this->sharedState = $sharedState;
    }

    public function operation($uniqueState)
    {
        $s = json_encode($this->sharedState);
        $u = json_encode($uniqueState);
        print("Flyweight: Displaying shared ($s) and unique ($u) state.\n");
    }
}

/**
 * Фабрика Легковесов создает объекты-Легковесы и управляет ими. Она
 * обеспечивает правильное разделение легковесов. Когда клиент запрашивает
 * легковес, фабрика либо возвращает существующий экземпляр, либо создает новый,
 * если он ещё не существует.
 */
class FlyweightFactory
{
    /**
     * @var Flyweight[]
     */
    private $flyweights = [];

    public function __construct(array $initialFlyweights)
    {
        foreach ($initialFlyweights as $state) {
            $this->flyweights[$this->getKey($state)] = new Flyweight($state);
        }
    }

    /**
     * Возвращает хеш строки Легковеса для данного состояния.
     *
     * @param array $state
     * @return string
     */
    private function getKey(array $state)
    {
        ksort($state);

        return implode("_", $state);
    }

    /**
     * Возвращает существующий Легковес с заданным состоянием или создает новый.
     *
     * @param $sharedState
     * @return Flyweight
     */
    public function getFlyweight(array $sharedState)
    {
        $key = $this->getKey($sharedState);

        if (! isset($this->flyweights[$key])) {
            print("FlyweightFactory: Can't find a flyweight, creating new one.\n");
            $this->flyweights[$key] = new Flyweight($sharedState);
        } else {
            print("FlyweightFactory: Reusing existing flyweight.\n");
        }

        return $this->flyweights[$key];
    }

    public function listFlyweights()
    {
        $count = count($this->flyweights);
        print("\nFlyweightFactory: I have $count flyweights:\n");
        foreach ($this->flyweights as $key => $flyweight) {
            print($key."\n");
        }
    }
}

/**
 * Клиентский код обычно создает кучу предварительно заполненных легковесов на
 * этапе инициализации приложения.
 */
$factory = new FlyweightFactory([
    ["Chevrolet", "Camaro2018", "pink"],
    ["Mercedes Benz", "C300", "black"],
    ["Mercedes Benz", "C500", "red"],
    ["BMW", "M5", "red"],
    ["BMW", "X6", "white"],
    // ...
]);
$factory->listFlyweights();

// ...

function addCarToPoliceDatabase(
    FlyweightFactory $ff, $plates, $owner,
    $brand, $model, $color
) {
    print("\nClient: Adding a car to database.\n");
    $flyweight = $ff->getFlyweight([$brand, $model, $color]);

    // Клиентский код либо сохраняет, либо вычисляет внешнее состояние и
    // передает его методам легковеса.
    $flyweight->operation([$plates, $owner]);
}

addCarToPoliceDatabase($factory,
    "CL234IR",
    "James Doe",
    "BMW",
    "M5",
    "red");

addCarToPoliceDatabase($factory,
    "CL234IR",
    "James Doe",
    "BMW",
    "X1",
    "red");

$factory->listFlyweights();

Output.txt: Результат выполнения

FlyweightFactory: I have 5 flyweights:
Chevrolet_Camaro2018_pink
Mercedes Benz_C300_black
Mercedes Benz_C500_red
BMW_M5_red
BMW_X6_white

Client: Adding a car to database.
FlyweightFactory: Reusing existing flyweight.
Flyweight: Displaying shared (["BMW","M5","red"]) and unique (["CL234IR","James Doe"]) state.

Client: Adding a car to database.
FlyweightFactory: Can't find a flyweight, creating new one.
Flyweight: Displaying shared (["BMW","X1","red"]) and unique (["CL234IR","James Doe"]) state.

FlyweightFactory: I have 6 flyweights:
Chevrolet_Camaro2018_pink
Mercedes Benz_C300_black
Mercedes Benz_C500_red
BMW_M5_red
BMW_X6_white
BMW_X1_red

Пример: Пример из жизни

Прежде чем мы начнём, обратите внимание, что реальное применение паттерна Легковес на PHP встречается довольно редко. Это связано с однопоточным характером PHP, где вы не должны хранить ВСЕ объекты вашего приложения в памяти одновременно в одном потоке. Хотя замысел этого примера только наполовину серьёзен, и вся проблема с ОЗУ может быть решена, если приложение структурировать по-другому, он всё же наглядно показывает концепцию паттерна, как он работает в реальном мире. Итак, я вас предупредил. Теперь давайте начнём.

В этом примере паттерн Легковес применяется для минимизации использования оперативной памяти объектами в базе данных животных ветеринарной клиники только для кошек. Каждую запись в базе данных представляет объект-Кот. Его данные состоят из двух частей:

  1. Уникальные (внешние) данные: имя кота, возраст и инфо о владельце.
  2. Общие (внутренние) данные: название породы, цвет, текстура и т.д.

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

FlyweightRealWorld.php: Пример из жизни

<?php

namespace RefactoringGuru\Flyweight\RealWorld;

/**
 * Паттерн Легковес
 *
 * Назначение: Позволяет вместить бóльшее количество объектов в отведённую
 * оперативную память. Легковес экономит память, разделяя общее состояние
 * объектов между ними, вместо хранения одинаковых данных в каждом объекте.
 *
 * Пример: Прежде чем мы начнём, обратите внимание, что реальное применение
 * паттерна Легковес на PHP встречается довольно редко. Это связано с
 * однопоточным характером PHP, где вы не должны хранить ВСЕ объекты вашего
 * приложения в памяти одновременно в одном потоке. Хотя замысел этого примера
 * только наполовину серьёзен, и вся проблема с ОЗУ может быть решена, если
 * приложение структурировать по-другому, он всё же наглядно показывает
 * концепцию паттерна, как он работает в реальном мире. Итак, я вас предупредил.
 * Теперь давайте начнём.
 *
 * В этом примере паттерн Легковес применяется для минимизации использования
 * оперативной памяти объектами в базе данных животных ветеринарной клиники
 * только для кошек. Каждую запись в базе данных представляет объект-Кот. Его
 * данные состоят из двух частей:
 *
 * 1. Уникальные (внешние) данные: имя кота, возраст и инфо о владельце.
 * 2. Общие (внутренние) данные: название породы, цвет, текстура и т.д.
 *
 * Первая часть хранится непосредственно внутри класса Кот, который играет роль
 * контекста. Вторая часть, однако, хранится отдельно и может совместно
 * использоваться разными объектами котов. Эти совместно используемые данные
 * находятся внутри класса РазновидностиКотов. Все коты, имеющие схожие
 * признаки, привязаны к одному и тому же классу РазновидностейКотов, вместо
 * того чтобы хранить повторяющиеся данные в каждом из своих объектов.
 */

/**
 * Объекты Легковеса представляют данные, разделяемые несколькими объектами
 * Кошек.  Это сочетание породы, цвета, текстуры и т.д.
 */
class CatVariation
{
    /**
     * Так называемое «внутреннее» состояние.
     */
    public $breed;

    public $image;

    public $color;

    public $texture;

    public $fur;

    public $size;

    public function __construct($breed, $image, $color, $texture, $fur, $size)
    {
        $this->breed = $breed;
        $this->image = $image;
        $this->color = $color;
        $this->texture = $texture;
        $this->fur = $fur;
        $this->size = $size;
    }

    /**
     * Этот метод отображает информацию о кошке. Метод принимает внешнее
     * состояние в качестве аргументов. Остальная часть состояния хранится
     * внутри полей Легковеса.
     *
     * Возможно, вы удивлены, почему мы поместили основную логику кошки в класс
     * РазновидностейКошек вместо того, чтобы держать её в классе Кошки. Я
     * согласен, это звучит странно.
     *
     * Имейте в виду, что в реальной жизни паттерн Легковес может быть либо
     * реализован с самого начала, либо принудительно применён к существующему
     * приложению,  когда разработчики понимают, что они столкнулись с проблемой
     * ОЗУ.
     *
     * Во втором случае вы получаете такие же классы, как у нас. Мы как бы
     * «отрефакторили» идеальное приложение, где все данные изначально
     * находились внутри класса Кошки. Если бы мы реализовывали Легковес с
     * самого начала, названия наших классов могли бы быть другими и более
     * определёнными. Например, Кошка и КонтекстКошки.
     *
     * Однако действительная причина, по которой основное поведение должно
     * проживать в классе Легковеса, заключается в том, что у вас может вообще
     * не быть  объявленного класса Контекста. Контекстные данные могут
     * храниться в массиве или какой-то другой, более эффективной структуре
     * данных.
     */
    public function renderProfile($name, $age, $owner)
    {
        print("= $name =\n");
        print("Age: $age\n");
        print("Owner: $owner\n");
        print("Breed: $this->breed\n");
        print("Image: $this->image\n");
        print("Color: $this->color\n");
        print("Texture: $this->texture\n");
    }
}

/**
 * Контекст хранит данные, уникальные для каждой кошки.
 *
 * Создавать отдельный класс для хранения контекста необязательно и не всегда
 * целесообразно. Контекст может храниться внутри громоздкой структуры данных в
 * коде Клиента и при необходимости передаваться в методы легковеса.
 */
class Cat
{
    /**
     * Так называемое «внешнее» состояние.
     */
    public $name;

    public $age;

    public $owner;

    /**
     * @var CatVariation
     */
    private $variation;

    public function __construct($name, $age, $owner, CatVariation $variation)
    {
        $this->name = $name;
        $this->age = $age;
        $this->owner = $owner;
        $this->variation = $variation;
    }

    /**
     * Поскольку объекты Контекста не владеют всем своим состоянием, иногда для
     * удобства вы можете реализовать несколько вспомогательных методов
     * (например, для сравнения нескольких объектов Контекста между собой).
     *
     * @param $query
     * @return bool
     */
    public function matches($query): bool
    {
        foreach ($query as $key => $value) {
            if (property_exists($this, $key)) {
                if ($this->$key != $value) {
                    return false;
                }
            } elseif (property_exists($this->variation, $key)) {
                if ($this->variation->$key != $value) {
                    return false;
                }
            } else {
                return false;
            }
        }

        return true;
    }

    /**
     * Кроме того, Контекст может определять несколько методов быстрого доступа,
     *  которые делегируют исполнение объекту-Легковесу. Эти методы могут быть
     * остатками реальных методов, извлечённых в класс Легковеса во время
     * массивного рефакторинга к паттерну Легковес.
     */
    public function render()
    {
        $this->variation->renderProfile($this->name, $this->age, $this->owner);
    }
}

/**
 * Фабрика Легковесов хранит объекты Контекст и Легковес, эффективно скрывая
 * любое упоминание о паттерне Легковес от клиента.
 */
class CatDataBase
{
    /**
     * Список объектов-кошек (Контексты).
     */
    private $cats = [];

    /**
     * Список вариаций кошки (Легковесы).
     */
    private $variations = [];

    /**
     * При добавлении кошки в базу данных мы сначала ищем существующую вариацию
     * кошки.
     */
    public function addCat($name, $age, $owner, $breed, $image, $color, $texture, $fur, $size)
    {
        $variation =
            $this->getVariation($breed, $image, $color, $texture, $fur, $size);
        $this->cats[] = new Cat($name, $age, $owner, $variation);
        print("CatDataBase: Added a cat ($name, $breed).\n");
    }

    /**
     * Возвращаем существующий вариант (Легковеса) по указанным данным или
     * создаём новый, если он ещё не существует.
     */
    public function getVariation($breed, $image, $color, $texture, $fur, $size): CatVariation
    {
        $key = $this->getKey(get_defined_vars());

        if (! isset($this->variations[$key])) {
            $this->variations[$key] =
                new CatVariation($breed, $image, $color, $texture, $fur, $size);
        }

        return $this->variations[$key];
    }

    /**
     * Эта функция помогает генерировать уникальные ключи массива.
     */
    private function getKey($data): string
    {
        return md5(implode("_", $data));
    }

    /**
     * Ищем кошку в базе данных, используя заданные параметры запроса.
     */
    public function findCat($query)
    {
        foreach ($this->cats as $cat) {
            if ($cat->matches($query)) {
                return $cat;
            }
        }
        print("CatDataBase: Sorry, your query does not yield any results.");
    }
}

/**
 * Клиентский код.
 */
$db = new CatDataBase();

print("Client: Let's see what we have in \"cats.csv\".\n");

// Чтобы увидеть реальный эффект паттерна, вы должны иметь большую базу данных с
// несколькими миллионами записей. Не стесняйтесь экспериментировать с кодом,
// чтобы увидеть реальные масштабы паттерна.
$handle = fopen(__DIR__."/cats.csv", "r");
$row = 0;
$columns = [];
while (($data = fgetcsv($handle)) !== false) {
    if ($row == 0) {
        for ($c = 0; $c < count($data); $c++) {
            $columnIndex = $c;
            $columnKey = strtolower($data[$c]);
            $columns[$columnKey] = $columnIndex;
        }
        $row++;
        continue;
    }

    $db->addCat(
        $data[$columns['name']],
        $data[$columns['age']],
        $data[$columns['owner']],
        $data[$columns['breed']],
        $data[$columns['image']],
        $data[$columns['color']],
        $data[$columns['texture']],
        $data[$columns['fur']],
        $data[$columns['size']]
    );
    $row++;
}
fclose($handle);

// ...

print("\nClient: Let's look for a cat named \"Siri\".\n");
$cat = $db->findCat(['name' => "Siri"]);
if ($cat) {
    $cat->render();
}

print("\nClient: Let's look for a cat named \"Bob\".\n");
$cat = $db->findCat(['name' => "Bob"]);
if ($cat) {
    $cat->render();
}

Output.txt: Результат выполнения

Client: Let's see what we have in "cats.csv".
CatDataBase: Added a cat (Steve, Bengal).
CatDataBase: Added a cat (Siri, Domestic short-haired).
CatDataBase: Added a cat (Fluffy, Maine Coon).

Client: Let's look for a cat named "Siri".
= Siri =
Age: 2
Owner: Alexander Shvets
Breed: Domestic short-haired
Image: /cats/domestic-sh.jpg
Color: Black
Texture: Solid

Client: Let's look for a cat named "Bob".
CatDataBase: Sorry, your query does not yield any results.