![Легковес](/images/patterns/cards/flyweight-mini.png?id=422ca8d2f90614dce810a8812c626698)
Легковес на PHP
Легковес — это структурный паттерн, который экономит память, благодаря разделению общего состояния, вынесенного в один объект, между множеством объектов.
Легковес позволяет экономить память, кешируя одинаковые данные, используемые в разных объектах.
Сложность:
Популярность:
Применимость: Весь смысл использования Легковеса — в экономии памяти. Поэтому, если в приложении нет такой проблемы, то вы вряд ли найдёте там примеры Легковеса.
Паттерн особенно редко актуален для PHP-приложений, из-за самой природы языка. Скрипты почти всегда работают с данными приложения частями, никогда не загружая все данные в память одновременно.
Признаки применения паттерна: Легковес можно определить по создающим методам класса, которые возвращают закешированные объекты, вместо создания новых.
Концептуальный пример
Этот пример показывает структуру паттерна Легковес, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
index.php: Пример структуры паттерна
<?php
namespace RefactoringGuru\Flyweight\Conceptual;
/**
* Легковес хранит общую часть состояния (также называемую внутренним
* состоянием), которая принадлежит нескольким реальным бизнес-объектам.
* Легковес принимает оставшуюся часть состояния (внешнее состояние, уникальное
* для каждого объекта) через его параметры метода.
*/
class Flyweight
{
private $sharedState;
public function __construct($sharedState)
{
$this->sharedState = $sharedState;
}
public function operation($uniqueState): void
{
$s = json_encode($this->sharedState);
$u = json_encode($uniqueState);
echo "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);
}
}
/**
* Возвращает хеш строки Легковеса для данного состояния.
*/
private function getKey(array $state): string
{
ksort($state);
return implode("_", $state);
}
/**
* Возвращает существующий Легковес с заданным состоянием или создает новый.
*/
public function getFlyweight(array $sharedState): Flyweight
{
$key = $this->getKey($sharedState);
if (!isset($this->flyweights[$key])) {
echo "FlyweightFactory: Can't find a flyweight, creating new one.\n";
$this->flyweights[$key] = new Flyweight($sharedState);
} else {
echo "FlyweightFactory: Reusing existing flyweight.\n";
}
return $this->flyweights[$key];
}
public function listFlyweights(): void
{
$count = count($this->flyweights);
echo "\nFlyweightFactory: I have $count flyweights:\n";
foreach ($this->flyweights as $key => $flyweight) {
echo $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
) {
echo "\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, где вы не должны хранить ВСЕ объекты вашего приложения в памяти одновременно в одном потоке. Хотя замысел этого примера только наполовину серьёзен, и вся проблема с ОЗУ может быть решена, если приложение структурировать по-другому, он всё же наглядно показывает концепцию паттерна, как он работает в реальном мире. Итак, я вас предупредил. Теперь давайте начнём.
В этом примере паттерн Легковес применяется для минимизации использования оперативной памяти объектами в базе данных животных ветеринарной клиники только для кошек. Каждую запись в базе данных представляет объект-Кот
. Его данные состоят из двух частей:
- Уникальные (внешние) данные: имя кота, возраст и инфо о владельце.
- Общие (внутренние) данные: название породы, цвет, текстура и т. д.
Первая часть хранится непосредственно внутри класса Кот
, который играет роль контекста. Вторая часть, однако, хранится отдельно и может совместно использоваться разными котами. Эти совместно используемые данные находятся внутри класса РазновидностиКотов
. Все коты, имеющие схожие признаки, привязаны к одному и тому же классу РазновидностиКотов
, вместо того чтобы хранить повторяющиеся данные в каждом из своих объектов.
index.php: Пример из реальной жизни
<?php
namespace RefactoringGuru\Flyweight\RealWorld;
/**
* Объекты Легковеса представляют данные, разделяемые несколькими объектами
* Кошек. Это сочетание породы, цвета, текстуры и т.д.
*/
class CatVariation
{
/**
* Так называемое «внутреннее» состояние.
*/
public $breed;
public $image;
public $color;
public $texture;
public $fur;
public $size;
public function __construct(
string $breed,
string $image,
string $color,
string $texture,
string $fur,
string $size
) {
$this->breed = $breed;
$this->image = $image;
$this->color = $color;
$this->texture = $texture;
$this->fur = $fur;
$this->size = $size;
}
/**
* Этот метод отображает информацию о кошке. Метод принимает внешнее
* состояние в качестве аргументов. Остальная часть состояния хранится
* внутри полей Легковеса.
*
* Возможно, вы удивлены, почему мы поместили основную логику кошки в класс
* РазновидностейКошек вместо того, чтобы держать её в классе Кошки. Я
* согласен, это звучит странно.
*
* Имейте в виду, что в реальной жизни паттерн Легковес может быть либо
* реализован с самого начала, либо принудительно применён к существующему
* приложению, когда разработчики понимают, что они столкнулись с проблемой
* ОЗУ.
*
* Во втором случае вы получаете такие же классы, как у нас. Мы как бы
* «отрефакторили» идеальное приложение, где все данные изначально
* находились внутри класса Кошки. Если бы мы реализовывали Легковес с
* самого начала, названия наших классов могли бы быть другими и более
* определёнными. Например, Кошка и КонтекстКошки.
*
* Однако действительная причина, по которой основное поведение должно
* проживать в классе Легковеса, заключается в том, что у вас может вообще
* не быть объявленного класса Контекста. Контекстные данные могут храниться
* в массиве или какой-то другой, более эффективной структуре данных.
*/
public function renderProfile(string $name, string $age, string $owner)
{
echo "= $name =\n";
echo "Age: $age\n";
echo "Owner: $owner\n";
echo "Breed: $this->breed\n";
echo "Image: $this->image\n";
echo "Color: $this->color\n";
echo "Texture: $this->texture\n";
}
}
/**
* Контекст хранит данные, уникальные для каждой кошки.
*
* Создавать отдельный класс для хранения контекста необязательно и не всегда
* целесообразно. Контекст может храниться внутри громоздкой структуры данных в
* коде Клиента и при необходимости передаваться в методы легковеса.
*/
class Cat
{
/**
* Так называемое «внешнее» состояние.
*/
public $name;
public $age;
public $owner;
/**
* @var CatVariation
*/
private $variation;
public function __construct(string $name, string $age, string $owner, CatVariation $variation)
{
$this->name = $name;
$this->age = $age;
$this->owner = $owner;
$this->variation = $variation;
}
/**
* Поскольку объекты Контекста не владеют всем своим состоянием, иногда для
* удобства вы можете реализовать несколько вспомогательных методов
* (например, для сравнения нескольких объектов Контекста между собой).
*/
public function matches(array $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(): string
{
$this->variation->renderProfile($this->name, $this->age, $this->owner);
}
}
/**
* Фабрика Легковесов хранит объекты Контекст и Легковес, эффективно скрывая
* любое упоминание о паттерне Легковес от клиента.
*/
class CatDataBase
{
/**
* Список объектов-кошек (Контексты).
*/
private $cats = [];
/**
* Список вариаций кошки (Легковесы).
*/
private $variations = [];
/**
* При добавлении кошки в базу данных мы сначала ищем существующую вариацию
* кошки.
*/
public function addCat(
string $name,
string $age,
string $owner,
string $breed,
string $image,
string $color,
string $texture,
string $fur,
string $size
) {
$variation =
$this->getVariation($breed, $image, $color, $texture, $fur, $size);
$this->cats[] = new Cat($name, $age, $owner, $variation);
echo "CatDataBase: Added a cat ($name, $breed).\n";
}
/**
* Возвращаем существующий вариант (Легковеса) по указанным данным или
* создаём новый, если он ещё не существует.
*/
public function getVariation(
string $breed,
string $image, $color,
string $texture,
string $fur,
string $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(array $data): string
{
return md5(implode("_", $data));
}
/**
* Ищем кошку в базе данных, используя заданные параметры запроса.
*/
public function findCat(array $query)
{
foreach ($this->cats as $cat) {
if ($cat->matches($query)) {
return $cat;
}
}
echo "CatDataBase: Sorry, your query does not yield any results.";
}
}
/**
* Клиентский код.
*/
$db = new CatDataBase();
echo "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);
// ...
echo "\nClient: Let's look for a cat named \"Siri\".\n";
$cat = $db->findCat(['name' => "Siri"]);
if ($cat) {
$cat->render();
}
echo "\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.