Proxy (Pełnomocnik)

proxy uml

Opis

Wzorzec projektowy Proxy (Pełnomocnik) należy do grupy wzorców strukturalnych. Jest swego rodzaju nakładką, która chroni oryginalny obiekt. Może sterować dostępem do niego lub pełnić rolę opakowania, które go przechowuje. Pewnego rodzaju cache w przypadku, gdy inicjalizacja obiektu jest kosztowna.

Wzorzec Proxy można kojarzyć z popularnych ORM jak na przykład Doctrine. W momencie pobierania danych z bazy i ich hydracji, domyślnie wszystkie obiekty powiązane relacją są zastępowane obiektami proxy, czyli dziedziczącymi po oryginalnej encji. To tak zwany lazy-loading, który nie zawsze jest pożądany i może prowadzić do problemu N+1.

Problem i rozwiązanie

Niektóre operacje są szczególnie kosztowne jeśli chodzi o zasoby. Dążąc do optymalizacji pewnych procesów, często okaże się, że docelowym rozwiązaniem jest jak najrzadsze ich użycie. Niech wykonują się tylko wtedy, kiedy muszą, a w innym przypadku mogą działać na stanie zapisanym w pamięci.

Klasa proxy jest o tyle fajna, że implementuję wspólny interfejs z oryginalnym obiektem. Zapewnia to możliwość podmiany dowolnie we wszystkich miejscach. Proxy dla encji to świetne wykorzystanie tego wzorca projektowego. Wystarczy sobie wyobrazić, że pewien obiekt jest potrzebny tylko do operacji zmiany jednego jego parametru – na przykład nazwy. Nie ma więc sensu targać wszystkich innych tym samym opóźniając czas oczekiwania.

Inny problem to kontrola dostępu do zasobu. Nakładka może nim sterować, a oryginalny obiekt nie musi nic o tym wiedzieć. Wykona swoje zadanie wtedy, kiedy zostanie o to poproszony. Mechanizm opiera się na delegowaniu, a dodatkowo wszędzie tam gdzie trzeba wykonać coś tuż przed albo tuż po, bierze się do roboty.

Plusy i minusy

To rozwiązanie nie ciągnie ze sobą wielu minusów. Nawet, gdy się nie sprawdzi w bardzo łatwy sposób można się go pozbyć z kodu. Jedyna wada jakiej można się doszukać to nadmiarowość, która ma miejsce wyłącznie w przypadku, gdy oddelegowanie zadania do oryginalnego obiektu ma miejsce tylko raz. Wówczas proxy jest tworzony dodatkowo, co czyni go zbędną warstwą.

Proxy to wzorzec projektowy, który łatwo pozwala rozszerzać funkcjonalności (open-closed) innych klas, co jest szczególnie przydatne przy użyciu zewnętrznych bibliotek. Ma jedną konkretną odpowiedzialność (single responsibility). Może opóźnić tworzenie instancji klasy do odpowiedniego momentu, a dalej zapewnić optymalizację w kolejnych jego żądaniach.

Jest bardzo niezależny, co oznacza że pełnomocników dla danej klasy może być tyle, ile potrzeba. Jeden może odpowiadać za dostęp, a dwa pozostałe implementować inny rodzaj cache i tak dalej. Tak naprawdę istnieje zupełna dowolność, a poprzez wspólny interfejs mechanizm jest łatwo wymienialny.

Przykładowa implementacja w PHP

Wzorzec projektowy Pełnomocnik jest prosty do zaimplementowania w PHP. Niektóre implementacje mogą nawet nie rozbijać tego mechanizmu na dwie klasy, a zawierać logikę proxy wewnątrz oryginalnego obiektu. Mi bliższa jest implementacja, którą prezentuję, jako że bazuje mocno na zasadach SOLID.

Dodatkowo ten mechanizm często używany jest do obsługi obiektów klas z zewnętrznych bibliotek, gdzie nie ma możliwości edycji oryginalnego kodu. W takim wypadku pojawia się też problem wspólnego interfejsu, który jest jednym z założeń wzorca. Nie zawsze będzie dało się go uzyskać poprzez kompozycję i wówczas to dziedziczenie może okazać się planem B.

Poniższa implementacja jest najprostsza, ale skuteczna. Jeżeli kodowanie pliku graficznego do base64 okaże się czasochłonne to sensowniej będzie ten stan zatrzymać w pamięci zamiast przed każdą operacją powielać to działanie. W kodzie klienckim to bez różnicy z jakiego źródła pochodzą dane dopóki faktycznie są zgodne. Ostatecznie to i tak funkcjonalność oryginalnej klasy jest pożądaną logiką, a obiekt proxy tylko steruje jego cyklem życia.

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Proxy;

interface Base64EncodingInterface
{
    public function encodeBase64(string $path): string;
}

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Proxy;

class FileEncoder implements Base64EncodingInterface
{
    /**
     * @throws FileNotFoundException
     */
    public function encodeBase64(string $path): string
    {
        $file = file_get_contents($path);

        if (false === $file) {
            throw new FileNotFoundException(sprintf('File does not exist, path: %s', $path));
        }

        return base64_encode($file);
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Proxy;

use Exception;

class FileNotFoundException extends Exception
{
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Proxy;

final class FileEncoderProxy implements Base64EncodingInterface
{
    private array $cache = [];
    
    public function __construct(
        private FileEncoder $fileEncoder
    ) {}

    public function encodeBase64(string $path): string
    {
        if (empty($this->cache[$path])) {
            $this->cache[$path] = $this->fileEncoder->encodeBase64($path);
        }

        return $this->cache[$path];
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Proxy\Test;

use DesignPatterns\Structural\Proxy\FileEncoder;
use DesignPatterns\Structural\Proxy\FileEncoderProxy;
use PHPUnit\Framework\TestCase;

final class FileEncoderProxyTest extends TestCase
{
    public function testProxyDoesNotChangeResult(): void
    {
        $fileTestPath = 'https://koddlo.pl/images/test.png';
        $base64example = 'S29kZGxv';
        $fileEncoder = $this->createMock(FileEncoder::class);
        $fileEncoder
            ->expects($this->any())
            ->method('encodeBase64')
            ->with($fileTestPath)
            ->willReturn($base64example);
        $fileEncoderProxy = new FileEncoderProxy($fileEncoder);

        self::assertSame($base64example, $fileEncoderProxy->encodeBase64($fileTestPath));
    }

    public function testProxyCacheResult(): void
    {
        $fileTestPath = 'https://koddlo.pl/images/test.png';
        $base64example = 'S29kZGxv';
        $fileEncoder = $this->createMock(FileEncoder::class);
        $fileEncoder
            ->expects($this->once())
            ->method('encodeBase64')
            ->with($fileTestPath)
            ->willReturn($base64example);
        $fileEncoderProxy = new FileEncoderProxy($fileEncoder);
        $fileEncoderProxy->encodeBase64($fileTestPath);
        $fileEncoderProxy->encodeBase64($fileTestPath);
        $fileEncoderProxy->encodeBase64($fileTestPath);

        self::assertSame($base64example, $fileEncoderProxy->encodeBase64($fileTestPath));
    }
}

Proxy – podsumowanie

Ogólnie wyróżnia się kilka rodzajów wzorca Proxy, ale wydaje mi się że nie ma sensu się w nie zagłębiać. Każdy pattern można implementować na wiele sposób po to by rozwiązać konkretny problem. Ten tak jak wspomniałem pomaga w sterowaniu cyklem życia i dostępem do oryginalnego obiektu.

Proxy należy do łatwiejszych wzorców projektowych, więc przeczytanie wpisu powinno wystarczyć do jego zapamiętania i mam nadzieję zastosowania w praktyce. To jeden z nielicznych mechanizmów, który może być używany w nadmiarze. Po prostu ma swoją określoną funkcję i raczej nie będzie pasował do innych okoliczności.

Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan dobrego humoru i podcastów.