
Pyłek w języku PHP
Pyłek to strukturalny wzorzec projektowy umożliwiający obsługę wielkich ilości obiektów przy jednoczesnej oszczędności pamięci.
Wzorzec Pyłek umożliwia zmniejszenie wymogów w zakresie pamięci RAM poprzez współdzielenie części opisu stanu przez wiele obiektów. Innymi słowy Pyłek przechowuje w pamięci podręcznej te dane, które są wspólne dla wielu różnych obiektów.
Złożoność:
Popularność:
Przykłady użycia: Pyłek jest rzadko używany w aplikacjach PHP z racji samej natury tego języka. Skrypt PHP na ogół pracuje na pewnej części danych aplikacji i nigdy nie wczytuje wszystkich danych do pamięci naraz.
Identyfikacja: Pyłek można poznać po obecności metody kreacyjnej zwracającej obiekty z pamięci podręcznej zamiast nowo utworzonych.
Przykład koncepcyjny
Poniższy przykład ilustruje strukturę wzorca Pyłek ze szczególnym naciskiem na następujące kwestie:
- Z jakich składa się klas?
- Jakie role pełnią te klasy?
- W jaki sposób elementy wzorca są ze sobą powiązane?
Poznawszy strukturę wzorca będzie ci łatwiej zrozumieć następujący przykład, oparty na prawdziwym przypadku użycia PHP.
index.php: Przykład koncepcyjny
<?php
namespace RefactoringGuru\Flyweight\Conceptual;
/**
* 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): void
{
$s = json_encode($this->sharedState);
$u = json_encode($uniqueState);
echo "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): string
{
ksort($state);
return implode("_", $state);
}
/**
* Returns an existing Flyweight with a given state or creates a new one.
*/
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";
}
}
}
/**
* 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
) {
echo "\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: Wynik działania
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
Przykład z prawdziwego życia
Zanim zaczniemy, zauważ, że prawdziwych zastosowań wzorca Pyłek w PHP jest niewiele. Wynika to z jednowątkowej natury PHP oraz faktu, że nie przechowuje się w pamięci WSZYSTKICH obiektów aplikacji jednocześnie i w obrębie jednego wątku. Sama idea tego przykładu jest nieco żartobliwa, a problem pamięci można rozwiązać zmieniając strukturę aplikacji, ale niemniej jednak mamy tu okazję zobaczyć jak wzorzec Pyłek może być użyty w prawdziwym życiu. Tyle, jeśli chodzi o rację bytu tego przykładu. Zaczynamy.
W tym przykładzie wzorzec Pyłek zastosowano w celu zminimalizowania zużycia pamięci RAM przez obiekty bazy danych kliniki weterynaryjnej dla kotów. Każdy wpis w bazie danych jest reprezentowany przez obiekt Cat
. Jego dane składają się z dwóch części:
- Unikalne (zewnętrzne) dane, jak imię, wiek i informacje o właścicielu.
- Współdzielone (wewnętrzne) dane, jak rasa, barwa, umaszczenie, itp.
Pierwsza część jest przechowywana bezpośrednio w obrębie klasy Cat
, pełniącej rolę kontekstu. Druga część danych znajduje się w osobnym miejscu i może odnosić się do wielu kotów. Dane współdzielone umieszczono w klasie CatVariation
. Wszystkie koty mające podobne cechy są powiązane z tą samą klasą CatVariation
, a powtarzające się dane nie są powielane w każdym obiekcie.
index.php: Przykład z prawdziwego życia
<?php
namespace RefactoringGuru\Flyweight\RealWorld;
/**
* 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(
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;
}
/**
* 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(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";
}
}
/**
* 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(string $name, string $age, string $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.)
*/
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;
}
/**
* 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(): string
{
$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(
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";
}
/**
* Return an existing variation (Flyweight) by given data or create a new
* one if it doesn't exist yet.
*/
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];
}
/**
* This function helps to generate unique array keys.
*/
private function getKey(array $data): string
{
return md5(implode("_", $data));
}
/**
* Look for a cat in the database using the given query parameters.
*/
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.";
}
}
/**
* The client code.
*/
$db = new CatDataBase();
echo "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);
// ...
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: Wynik działania
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.