/ デザインパターン / Flyweight / PHP Flyweight を PHP で Flyweight は、 構造に関するデザインパターンの一つで、 メモリー消費量を低く抑えることで、 プログラムが膨大な数のオブジェクトを支えることができるようにします。 複数のオブジェクト間でオブジェクトの状態の一部を共有することにより、 これを実現します。 つまり、 Flyweight は、 異なるオブジェクトによって使われる同じデータをキャッシュすることにより、 RAM を節約します。 Flyweight の詳細 ナビゲーション はじめに 概念的な例 index Output 現実的な例 index Output 複雑度: 人気度: 使用例: Flyweight は、 PHP アプリケーションでは特に稀です。 これは、 言語の性質のためと言えます。 典型的な PHP スクリプトは、 アプリケーション・データの一部だけを操作し、 全データを同時にメモリーに読み込むことはありません。 見つけ方: Flyweight は、 新規オブジェクトの代わりにキャッシュされたオブジェクトを返す生成メソッドの存在により識別できます。 概念的な例 現実的な例 概念的な例 この例は、 Flyweight デザインパターンの構造を説明するためのものです。 以下の質問に答えることを目的としています: どういうクラスからできているか? それぞれのクラスの役割は? パターンの要素同士はどう関係しているのか? ここでパターンの構造を学んだ後だと、 これに続く、 現実世界の PHP でのユースケースが理解しやすくなります。 index.php: 概念的な例 <?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: 実行結果 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 での Flyweight パターンの実際の適用はかなり珍しいということを念頭に置いてください。 これは、 PHP がシングル・スレッドであるということに起因します。 同一スレッド内で同時にすべてのアプリケーション・オブジェクトをメモリーに置くということは、 行うべきことではありません。 この例は、 実際のところあまり現実的ではなく、 RAM 不足問題はアプリの構造の変更で解決できるかもしれません。 しかしそれでも、 概念的には、 現実世界でのパターンの使用例として使えると思います。 という断り書き付きで、 では始めましょう。 この例では、 猫専門の動物病院の動物データベースで、 オブジェクトによる RAM 使用量を最小限に抑えるために、 Flyweight パターンを使用しています。 データベース内の各レコードは、 Cat オブジェクトで表されます。 そのデータは以下の二つの部分で構成されています: ペットの名前、 年齢、 飼い主情報などの固有 (外因的) データ 品種名、 色、 質感などの共有 (内因的) データ 最初の部分は、 Cat クラス内に直接収められ、 コンテキストの働きをします。 二つ目の部分は、 別個に蓄えられ、 複数の猫が共有します。 共有部分は、 CatVariation クラスに収められます。 類似の特徴のある猫は全部同じCatVariation オブジェクトに結合し、 重複データをそれぞれのオブジェクトに格納することを避けることができます。 index.php: 現実的な例 <?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: 実行結果 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. 概念的な例 現実的な例