wzorzec pyłek

Wzorca projektowego pyłek (ang. flyweight) można użyć, jeśli mamy naprawdę dużo małych obiektów (małych jak muchy), które różnią się tylko podanym stanem lub wieloma danymi, które działają na podobnych danych wejściowych (lub część tego stanu jest powtarzalna).

Wyobraź sobie, że importujesz ogromny plik CSV z kilkoma tysiącami wierszy informacji o telewizorach rozmieszczonych po całej Europie. Twoim zadaniem jest sprawdzanie kondycji każdego z nich za pomocą pojedynczego połączenia z jego adresem IP i zalogowanie informacji o tym urządzeniu.

Kilka tysięcy nowych obiektów. To naprawdę sporo danych do przechowywania w pamięci RAM. Na szczęście duża część danych w wierszach jest powtarzalna. Możemy ponownie wykorzystywać obiekty, które już stworzyliśmy i pracować na nich. To esencja wzorca pyłek.

Zacznijmy od utworzenia przykładowego raportu. Mamy wiele różnych telewizorów umieszczonych w wielu lokalizacjach. Każdy z nich ma unikalny identyfikator użytkownika i kilka innych informacji. W tym przykładzie utworzymy 5 tys. wierszy danych.

<?php
class CSV {
    const COLUMNS = ["id", "uuid", "location", "resolution", "producer", "operating_system", "ip"];
    const LOCATIONS = ["Warsaw", "Berlin", "Amsterdam", "Paris"];
    const RESOLUTIONS = ["Full HD", "4K"];
    const PRODUCERS = ["LG", "Samsung", "Philips", "Sencor"];
    const OPERATING_SYSTEMS = ["Linux", "Android", "Ubuntu"];
    private $numItems;
    private $fileName;
    private $file;
    public function __construct (string $fileName, int $numItems) {
        $this->numItems = $numItems;
        $this->fileName = $fileName;
    }
    protected function createHeader (): void {
        fputcsv($this->file, self::COLUMNS);
    }
    protected function generateRandomString (int $length): string {
        return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);
    }
    protected function getRand (array $arr): string {
        $key = array_rand($arr);
        return $arr[$key];
    }
    protected function getRandIp () {
        return mt_rand(0, 255) . "." . mt_rand(0, 255) . "." . mt_rand(0, 255) . "." . mt_rand(0, 255);
    }
    protected function generateData (): void {
        for ($idx = 0; $idx < $this->numItems; $idx++) {
            fputcsv($this->file, [
                $idx + 1,
                $this->generateRandomString(rand(5, 10)),
                $this->getRand(self::LOCATIONS),
                $this->getRand(self::RESOLUTIONS),
                $this->getRand(self::PRODUCERS),
                $this->getRand(self::OPERATING_SYSTEMS),
                $this->getRandIp()
            ]);
        }
    }
    public function create (): void {
        $this->file = fopen($this->fileName, 'w');
        $this->createHeader();
        $this->generateData();
        fclose($this->file);
    }
}
$csv = new CSV("demo.csv", 5000);
$csv->create();


Nasz raport jest już gotowy. Teraz musimy dowiedzieć się, jak stworzyć aplikację, która będzie odpytywać każde urządzenie. Zauważyliśmy, że każdy ma unikalny identyfikator użytkownika i adres IP. Inne pola są dość powtarzalne i możemy je przechowywać w osobnym obiekcie. Nazwijmy to DeviceType.

<?php
namespace structural\flyweight;
class DeviceType {
    protected $location;
    protected $resolution;
    protected $producer;
    protected $operatingSystem;
    public function __construct (
        string $location,
        string $resolution,
        string $producer,
        string $operatingSystem
    ) {
        $this->location = $location;
        $this->resolution = $resolution;
        $this->producer = $producer;
        $this->operatingSystem = $operatingSystem;
    }
    public function reportType () {
        return "Working on device in {$this->location} with resolution {$this->resolution} crated by {$this->producer} and running {$this->operatingSystem}";
    }
}


Ponieważ możemy ponownie wykorzystywać obiekty, stwórzmy fabrykę, która będzie odpowiedzialna za cache'owanie ich i tworzenie nowych w razie potrzeby.

<?php
namespace structural\flyweight;
class DeviceTypeFactory {
    protected $deviceTypes = [];
    public function getType (
        string $location,
        string $resolution,
        string $producer,
        string $operatingSystem): DeviceType {
        $key = $this->getKey(
            $location,
            $resolution,
            $producer,
            $operatingSystem);
        if (!array_key_exists($key, $this->deviceTypes)) {
            $this->deviceTypes[$key] = new DeviceType(
                $location,
                $resolution,
                $producer,
                $operatingSystem
            );
        }
        return $this->deviceTypes[$key];
    }
    protected function getKey (
        string $location,
        string $resolution,
        string $producer,
        string $operatingSystem) {
        return md5(implode("_", func_get_args()));
    }
}

Rozpoznajemy każdy typ urządzenia, tworząc hash z wszystkich jego parametrów. Teraz stwórzmy nasze urządzenie.

<?php
namespace structural\flyweight;
class Device {
    protected $uid;
    protected $ip;
    protected $type;
    public function __construct (
        string $uid,
        string $ip,
        DeviceType $type
    ) {
        $this->uid = $uid;
        $this->ip = $ip;
        $this->type = $type;
    }
    public function ping () {
        echo "Checking if device {$this->uid} is active" . PHP_EOL;
        $this->type->reportType() . PHP_EOL;
        echo "Calling it on ip {$this->ip}" . PHP_EOL;
    }
}


Ten kod odpowiada za sprawdzenie stanu każdego urządzenia. Urządzenia te tworzone są za pomocą DeviceStorage, który również śledzi wszystkie utworzone obiekty.

<?php
namespace structural\flyweight;
class DeviceStorage {
    public $devices = [];
    public $deviceFactory;
    public function __construct () {
        $this->deviceFactory = new DeviceTypeFactory();
    }
    public function addDevice (
        string $uuid,
        string $location,
        string $resolution,
        string $producer,
        string $operatingSystem,
        string $ip
    ) {
        $type = $this->deviceFactory->getType(
            $location,
            $resolution,
            $producer,
            $operatingSystem
        );
        $this->devices[] = new Device($uuid, $ip, $type);
    }
    public function checkDevicesHealth () {
        foreach ($this->devices as $device) {
            $device->ping();
        }
    }
}

Złóżmy to w całość.

<?php
namespace structural\flyweight;
//require ...
$file = fopen('demo.csv', 'r');
$devicesDB = new DeviceStorage();

for ($i = 0; $row = fgetcsv($file); ++$i) {
    // Omitting file headers
    if ($i) {
        $devicesDB->addDevice(
            $row[1],
            $row[2],
            $row[3],
            $row[4],
            $row[5],
            $row[6]
        );
    }
}

fclose($file);
$devicesDB->checkDevicesHealth();
echo memory_get_usage() / 1024 / 1024 . " RAM USED";

Aplikacja z wykorzystaniem Flyweight zużyła około 2,1 MB na zestawie danych 5 tys. I 13,6 MB na zestawie danych 50 tys.

Bez użycia współużytkowanego DeviceType było to 3,3 MB dla 5k i 25,7 MB dla 50k. To ogromna oszczędność.

Pyłek jest rzadko używany w aplikacjach webowych PHP, ponieważ każde żądanie w PHP jest całkowicie niezależne. Często nie przechowujemy danych bezpośrednio w pamięci RAM, a raczej w niektórych trwałych bazach danych lub pamięci podręcznej. Niemniej jednak ten wzorzec może być całkiem przydatny w konkretnych przypadkach użycia lub aplikacjach wiersza poleceń.


Oryginał tekstu w języku angielskim przeczytasz tutaj.

Komentarze wyłączone