Prototype (Prototyp)

prototype prototyp uml

Opis

Wzorzec projektowy Prototype (Prototyp) należy do grupy wzorców konstrukcyjnych. Bazuje na możliwości skopiowania prototypowej wersji obiektu i ewentualnym dostosowaniu go do potrzeb, zamiast tworzyć go od zera.

Szczególne przypadki, gdzie wzorzec będzie znajdował swoje miejsce, to te w których operacje wykonywane są na zestawie obiektów różniących się nieznacznie, a koszt ich wytworzenia jest dużo wyższy, niż klonowanie.

Problem i rozwiązanie

Jak można wykonać kopię jakiejś rzeczy? Trzeba zrobić ją od zera, a jedyne co można skopiować to proces jej wytwarzania, ale i tak trzeba będzie go powielić tyle razy, ile kopii jest potrzebnych. Tak samo można postąpić podczas kopiowania obiektu w programowaniu. Utworzyć nowy i skonfigurować go w taki sam sposób jak oryginalny. Tylko po co, skoro w bajkach i programowaniu możemy raz go utworzyć i magicznie sklonować w tylu egzemplarzach, ile potrzeba.

Co więcej, dzięki takiemu zabiegowi zyskać można niezależność od kopiowanej klasy. Klonowanie jest skupione i zaenkapsulowane wewnątrz klasy bazowej. Oczywiście, nic nie stoi na przeszkodzie, żeby logikę kopiowania zamknąć w serwisie i nadal uzależniać się od oryginalnego obiektu. Tyle, że to rodzi kolejne problemy. Co na przykład z prywatnymi polami klasy? OK, można skorzystać z getterów. Wszystko da się zrobić na wiele sposobów. Nie wiem jednak, po co się męczyć skoro wystarczy skorzystać z magicznej metody __clone().

Jeśli standardowe działanie funkcji nie jest wystarczające albo właściwe, to spokojnie można w niej zawrzeć własne reguły. Dodatkowo, w niektórych przypadkach klonowanie obiektu za pomocą wzorca Prototype okaże się wydajniejsze, niż jego inicjalizacja i ponowne ustawianie odpowiedniego stanu. To bardziej efekt uboczny wzorca, ale tak się składa, że często okaże się wyznacznikiem, czy warto go użyć.

Plusy i minusy

Tym razem zacznę od pozytywów. Tak jak już wspomniałem, wzorzec projektowy Prototyp może okazać się wydajniejszą opcją, aniżeli tworzenie obiektu od zera. Sam proces kopiowania jest też dużo mniej wiążący pod względem klas i ogranicza stosowanie żmudnego dziedziczenia. Pozwala dowolnie konfigurować proces klonowania. Zazwyczaj skończy się na części wspólnej i jak sama nazwa wzorca wskazuje, utworzony zostanie prototyp, który można dalej rozszerzać i konfigurować.

Taka funkcjonalność jest jak najbardziej reużywalna i zazwyczaj wygodna. Dlaczego zazwyczaj? W niektórych sytuacjach okaże się, że zbyt skomplikowany obiekt nie jest taki łatwy w odtworzeniu. I tutaj przechodzę do wad.

Pierwszy problem to fakt rozszerzania klasy i ciągłego dostosowywania metody __clone(). Najtrudniejsze jest chyba żeby o tym nie zapomnieć. Inne szczególnie problematyczne miejsca to powiązania z innymi obiektami. Czasem istnieje potrzeba sklonowania nie tylko obiektu bazowego, ale i powiązanych, a nie obiektu z referencjami do tych samych klas co oryginalny (jak dzieje się to domyślnie). Kolejny podobny przypadek, kiedy istnieje referencja z pierwszego miejsca w drugie, ale też z drugiego w pierwsze. Żeby to łatwiej zobrazować jako przykład podam relację bidirectional w Doctrine. Rozwiązania obu kłopotliwych sytuacji obrazuje przykładowy kod.

Przykładowa implementacja w PHP

Wzorzec Prototype w PHP jest dość łatwy w implementacji, dlatego że twórcy do dyspozycji przekazali magiczną metodę, która zrobi robotę. W praktyce wystarczy dostosować działający już mechanizm do własnych potrzeb. Wszystko rozchodzi się jednak o odpowiednie zlokalizowanie miejsca, w którym taka funkcjonalność zda egzamin. Dobrze jest też rozumieć działanie standardowego kopiowania, żeby nie popełnić podstawowych błędów.

<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Prototype;

final class City
{
    private string $id;

    public function __construct()
    {
        $this->id = uniqid();
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Prototype;

final class Invitation
{
    private string $id;

    public function __construct()
    {
        $this->id = uniqid();
    }

    public function getId(): string
    {
        return $this->id;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Prototype;

final class Place
{
    private string $id;

    private ?Event $event = null;

    public function __construct()
    {
        $this->id = uniqid();
    }

    public function setEvent(?Event $event): void
    {
        $this->event = $event;
    }

    public function getEvent(): ?Event
    {
        return $this->event;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Prototype;

interface EventPrototypeInterface
{
    public function __clone();
}

<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Prototype;

use DateTimeImmutable;
use DateTimeInterface;

final class Event implements EventPrototypeInterface
{
    private string $id;

    private int $budget = 0;

    private DateTimeImmutable $created;

    private array $invitations;

    public function __construct(
        private City $city,
        private Place $place
    ) {
        $this->id = uniqid();
        $place->setEvent($this);
        $this->created = new DateTimeImmutable();
        $this->invitations = [];
    }

    public function __clone()
    {
        $this->id = uniqid();
        $this->created = new DateTimeImmutable();
        $this->place = clone $this->place;
        $this->place->setEvent($this);
        $this->invitations = [];
    }

    public function getId(): string
    {
        return $this->id;
    }

    public function getCity(): City
    {
        return $this->city;
    }

    public function getPlace(): Place
    {
        return $this->place;
    }

    public function getBudget(): int
    {
        return $this->budget;
    }

    public function setBudget(int $budget): Event
    {
        $this->budget = $budget;

        return $this;
    }

    public function getCreated(): DateTimeInterface
    {
        return $this->created;
    }

    public function getInvitations(): array
    {
        return $this->invitations;
    }

    public function addInvitation(Invitation $invitation): Event
    {
        $this->invitations[$invitation->getId()] = $invitation;

        return $this;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Prototype\Test;

use DateTimeInterface;
use DesignPatterns\Creational\Prototype\City;
use DesignPatterns\Creational\Prototype\Event;
use DesignPatterns\Creational\Prototype\EventPrototypeInterface;
use DesignPatterns\Creational\Prototype\Invitation;
use DesignPatterns\Creational\Prototype\Place;
use PHPUnit\Framework\TestCase;

final class EventTest extends TestCase
{
    public function testIsEventValidAfterCreation(): void
    {
        $event = (new Event(new City(), new Place()))
            ->setBudget(1000000)
            ->addInvitation(new Invitation())
            ->addInvitation(new Invitation());

        self::assertInstanceOf(EventPrototypeInterface::class, $event);
        self::assertIsString($event->getId());
        self::assertInstanceOf(City::class, $event->getCity());
        self::assertInstanceOf(Place::class, $event->getPlace());
        self::assertSame(1000000, $event->getBudget());
        self::assertInstanceOf(DateTimeInterface::class, $event->getCreated());
        self::assertSame($event, $event->getPlace()->getEvent());
        self::assertCount(2, $event->getInvitations());
    }

    public function testIsConfiguredFineAfterClone(): void
    {
        $event = (new Event(new City(), new Place()))
            ->setBudget(1000000)
            ->addInvitation(new Invitation())
            ->addInvitation(new Invitation());

        $cloneEvent = clone $event;

        self::assertNotEquals($event, $cloneEvent);
        self::assertNotSame($event->getId(), $cloneEvent->getId());
        self::assertSame($event->getCity(), $cloneEvent->getCity());
        self::assertSame($event->getBudget(), $cloneEvent->getBudget());
        self::assertNotSame($event->getCreated(), $cloneEvent->getCreated());
        self::assertNotSame($event->getPlace(), $cloneEvent->getPlace());
        self::assertNotSame($event, $cloneEvent->getPlace()->getEvent());
        self::assertEmpty($cloneEvent->getInvitations());
    }
}

Prototype – podsumowanie

Przyznam bez bicia, że nie jest to wzorzec, z którego często korzystam. Chociaż sam nie wiem dlaczego, skoro wydaje się sensownym rozwiązaniem. Jakoś bliżej mi do innych wzorców kreacyjnych, ale podejście oparte na prototypie zawsze warto mieć w zanadrzu jako alternatywę właśnie dla innych bardziej skomplikowanych mechanizmów.

Krystian Żądło
Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan angielskiej piłki nożnej, dobrego humoru oraz podcastów.