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 zastosowanie, to te w których operacje wykonywane są na zestawie obiektów różniących się nieznacznie, a koszt ich wytworzenia jest dużo większy, niż klonowania.

Problem i rozwiązanie

Jak można wykonać kopię danej 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 tylko jest potrzebne.

Co więcej, dzięki takiemu zabiegowi zyskać można uniezależnić się 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? Dobra, 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 jest niewł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ą, niż 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. Zamiast skopiować obiekt 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ę dwukierunkową w Doctrine. Rozwiązania obu kłopotliwych sytuacji obrazuje przykładowy kod.

Przykładowa implementacja w PHP

Wzorzec Prototype w PHP jest łatwy w implementacji, dlatego że twórcy języka 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.

Przykładowa implementacja zawiera tworzenie bazowanego obiektu reprezentującego wydarzenie, które odbywa się w ramach określonego budżetu, w konkretnym miejscu i może mieć zgłoszenia. Klonowanie pozwala odtworzyć to samo wydarzenie bez zgłoszeń.

<?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

Chociaż mechanizm klonowania obiektu w PHP spotkać można w kodzie regularnie, to jednak w kontekście prototypowania obiektu już trochę rzadziej. Ten wzorzec projektowy z powodzeniem można wykorzystać w wielu miejscach, gdzie często tworzone są obiekty, a twory nie różnią się od siebie aż tak bardzo albo wywodzą się zawsze z części wspólnej.

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.

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