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

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

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

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

Детальніше про Легковаговика

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

Складність:

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

Застосування: Сенс використання Легковаговика — це економія пам'яті. Тому, якщо в програмі немає такої проблеми, ви навряд чи знайдете там приклади Легковаговика.

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

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

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

FlyweightStructural.php: Приклад структури патерну

<?php

namespace RefactoringGuru\Flyweight\Structural;

/**
 * The Flyweight stores a common portion of the state (also called intrinsic
 * state) that belongs to multiple real business entities. The Flyweight accepts
 * the rest of the state (extrinsic state, unique for each entity) via its
 * method parameters.
 */
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");
    }
}

/**
 * The Flyweight Factory creates and manages the Flyweight objects. It ensures
 * that flyweights are shared correctly. When the client requests a flyweight,
 * the factory either returns an existing instance or creates a new one, if it
 * doesn't exist yet.
 */
class FlyweightFactory
{
    /**
     * @var Flyweight[]
     */
    private $flyweights = [];

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

    /**
     * Returns a Flyweight's string hash for a given state.
     */
    private function getKey(array $state)
    {
        ksort($state);

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

    /**
     * Returns an existing Flyweight with a given state or creates a new one.
     */
    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");
        }
    }
}

/**
 * The client code usually creates a bunch of pre-populated flyweights in the
 * initialization stage of the application.
 */
$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]);

    // The client code either stores or calculates extrinsic state and passes it
    // to the flyweight's methods.
    $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

Приклад: Приклад з життя

FlyweightRealWorld.php: Приклад з життя

<?php

namespace RefactoringGuru\Flyweight\RealWorld;

/**
 * Flyweight Design Pattern
 *
 * Intent: Use sharing to fit more objects into the available amount of RAM by
 * sharing common parts of the object state among multiple objects, instead of
 * keeping the entire state in each object.
 *
 * Example: Before we begin, please note that real applications for the
 * Flyweight pattern in PHP are pretty rare. This stems from the single-thread
 * nature of PHP, where you're not supposed to be storing ALL of your
 * application's objects in memory at the same time, in the same thread. While
 * the idea for this example is only half-serious, and the whole RAM problem
 * might be solved by structuring the app differently, it still demonstrates the
 * concept of the pattern as it works in the real world. All right, I've given
 * you the disclaimer. Now, let's begin.
 *
 * In this example, the Flyweight pattern is used to minimize the RAM usage of
 * objects in an animal database of a cat-only veterinary clinic. Each record in
 * the database is represented by a Cat object. Its data consists of two parts:
 *
 * 1. Unique (extrinsic) data such as a pet's name, age, and owner info.
 * 2. Shared (intrinsic) data such as a breed name, color, texture, etc.
 *
 * The first part is stored directly inside the Cat class, which acts as a
 * context. The second part, however, is stored separately and can be shared by
 * multiple cats. This shareable data resides inside the CatVariation class. All
 * cats that have similar features are linked to the same CatVariation class,
 * instead of storing the duplicate data in each of their objects.
 */

/**
 * Flyweight objects represent the data shared by multiple Cat objects. This is
 * the combination of breed, color, texture, etc.
 */
class CatVariation
{
    /**
     * The so-called "intrinsic" state.
     */
    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;
    }

    /**
     * This method displays the cat information. The method accepts the
     * extrinsic  state as arguments. The rest of the state is stored inside
     * Flyweight's fields.
     *
     * You might be wondering why we had put the primary cat's logic into the
     * CatVariation class instead of keeping it in the Cat class. I agree, it
     * does sound confusing.
     *
     * Keep in mind that in the real world, the Flyweight pattern can either be
     * implemented from the start or forced onto an existing application
     * whenever the developers realize they've hit upon a RAM problem.
     *
     * In the latter case, you end up with such classes as we have here. We kind
     * of "refactored" an ideal app where all the data was initially inside the
     * Cat class. If we had implemented the Flyweight from the start, our class
     * names might be different and less confusing. For example, Cat and
     * CatContext.
     *
     * However, the actual reason why the primary behavior should live in the
     * Flyweight class is that you might not have the Context class declared at
     * all. The context data might be stored in an array or some other more
     * efficient data structure. You won't have another place to put your
     * methods in, except the Flyweight class.
     */
    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");
    }
}

/**
 * The context stores the data unique for each cat.
 *
 * A designated class for storing context is optional and not always viable. The
 * context may be stored inside a massive data structure within the Client code
 * and passed to the flyweight methods when needed.
 */
class Cat
{
    /**
     * The so-called "extrinsic" state.
     */
    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;
    }

    /**
     * Since the Context objects don't own all of their state, sometimes, for
     * the sake of convenience, you may need to implement some helper methods
     * (for example, for comparing several Context objects.)
     *
     * @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;
    }

    /**
     * The Context might also define several shortcut methods, that delegate
     * execution to the Flyweight object. These methods might be remnants of
     * real methods, extracted to the Flyweight class during a massive
     * refactoring to the Flyweight pattern.
     */
    public function render()
    {
        $this->variation->renderProfile($this->name, $this->age, $this->owner);
    }
}

/**
 * The Flyweight Factory stores both the Context and Flyweight objects,
 * effectively hiding any notion of the Flyweight pattern from the client.
 */
class CatDataBase
{
    /**
     * The list of cat objects (Contexts).
     */
    private $cats = [];

    /**
     * The list of cat variations (Flyweights).
     */
    private $variations = [];

    /**
     * When adding a cat to the database, we look for an existing cat variation
     * first.
     */
    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");
    }

    /**
     * Return an existing variation (Flyweight) by given data or create a new
     * one if it doesn't exist yet.
     */
    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];
    }

    /**
     * This function helps to generate unique array keys.
     */
    private function getKey($data): string
    {
        return md5(implode("_", $data));
    }

    /**
     * Look for a cat in the database using the given query parameters.
     */
    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.");
    }
}

/**
 * The client code.
 */
$db = new CatDataBase();

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

// To see the real effect of the pattern, you should have a large database with
// several millions of records. Feel free to experiment with code to see the
// real extent of the pattern.
$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.