16.12.20194 min

Jakub KapuścikCTO

Wzorzec projektowy pyłek w PHP

Sprawdź, w jakich przypadkach wykorzystać wzorzec projektowy pyłek i w jaki sposób można go użyć w PHP.

Wzorzec projektowy pyłek w PHP

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(__DIR__ . '/../../autoloader.php');
$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.

<p>Loading...</p>

Dziel się wiedzą ze 160 tysiącami naszych czytelników

Zostań autorem Readme

Hitachi Energy

Security Architect

senior

15 000 - 21 000 PLN

Umowa o pracę

Kraków

Praca zdalna 100%

Ważna do 26.02.2022

Bardzo dobrze
Microsoft Azure and/or AWS

Hitachi Energy

Product Development Manager

senior

15 000 - 20 000 PLN

Umowa o pracę

Kraków

Praca zdalna 100%

Ważna do 26.02.2022

Bardzo dobrze
AgileSoftware Development Life Cycle Leadership skills

Simple SA

Java Developer (Mid/Senior)

medium

7 000 - 15 000 PLN

Kontrakt B2BUmowa o pracę

Praca zdalna 100%

Ważna do 26.02.2022

Dobrze
JavaSpringSpring Boot

Asseco Poland S.A.

Administrator / Starszy Administrator Systemów IT

medium

Brak widełek

Kontrakt B2BUmowa o pracę

Praca zdalna 100%

Ważna do 26.02.2022

Dobrze
PostgreSQLBash

Nokia

5G Automation Engineer, IODT

medium

Brak widełek

Umowa o pracę

Wrocław

Praca zdalna 100%

Ważna do 13.03.2022

Divante

Senior Vue.js Developer

senior

15 300 - 23 500 PLN

Kontrakt B2BUmowa o pracę

Wrocław

Praca zdalna 100%

Ważna do 13.03.2022

Dobrze
JavaScriptTypeScriptVue.js

T-Mobile Polska S. A.

Frontend Developer

medium

Brak widełek

Kontrakt B2B

Warszawa

Ważna do 26.02.2022

Bardzo dobrze
ReactReduxNode.js

Commerzbank - Centrum Technologii Cyfrowych w Polsce

Business Expert for Risk Applications

medium

Znamy widełki

Umowa o pracę

Łódź

Ważna do 26.02.2022

Dobrze
SQLMS Office
Początkująco
SAS / R / Python

Commerzbank - Centrum Technologii Cyfrowych w Polsce

Business Expert with German for Risk Analytics

medium

Znamy widełki

Umowa o pracę

Łódź

Ważna do 26.02.2022

Dobrze
MS Office
Początkująco
SQL / VBA / PythonQlik Sense / Qlik View / Arcadia