Wiosenna WYPRZEDAŻ

Prototyp

Znany też jako: Klon, Clone, Prototype

Cel

Prototyp to kreacyjny wzorzec projektowy, który umożliwia kopiowanie już istniejących obiektów bez tworzenia zależności pomiędzy twoim kodem, a klasami obiektów.

Wzorzec Projektowy Prototyp

Problem

Przypuśćmy, że mamy jakiś obiekt i potrzebujemy jego dokładnej kopii. Jak można tego dokonać? Na początek, trzeba stworzyć nowy obiekt tej samej klasy. Potem zaś skopiować zawartość pól oryginału do pól kopii.

Fajnie! Ale jest i haczyk. Nie wszystkie obiekty można w ten sposób kopiować, bo nie wszystkie pola obiektu są ogólnodostępne — mogą być prywatne i tym samym niedostępne spoza samego obiektu.

Co może pójść nie tak gdy kopiujemy coś oglądając to wyłącznie z zewnątrz?

Kopiowanie obiektu oglądając go tylko z zewnątrz nie zawsze jest możliwe.

Jest jeszcze jeden kłopot z takim bezpośrednim podejściem. Skoro musisz wiedzieć, jakiej klasy jest obiekt który chcesz skopiować, to twój kod staje się zależny od tej klasy. Jeśli to nie wygląda na kłopot, to weź pod uwagę, że czasem znamy tylko interfejs obiektu, a nie jego konkretną klasę. Przykładem tego są metody przyjmujące w charakterze parametru obiekty zgodne z jakimś wspólnym interfejsem.

Rozwiązanie

Wzorzec projektowy Prototyp deleguje proces klonowania samym obiektom, które mają być sklonowane. We wzorcu tym deklarowany jest wspólny interfejs dla wszystkich obiektów wspierających funkcjonalność klonowania. Interfejs taki pozwala klonować obiekty bez konieczności sprzęgania kodu z klasą obiektu. Zazwyczaj taki interfejs ogranicza się tylko do pojedynczej metody klonuj.

Implementacja metody klonuj jest bardzo podobna w poszczególnych klasach. Metoda tworzy obiekt swojej klasy i kopiuje doń wartości jej pól. Można wówczas skopiować także pola prywatne, gdyż większość języków programowania pozwala obiektom na dostęp do prywatnych pól obiektów tej samej klasy.

Obiekt, który posiada funkcjonalność klonowania zwany jest prototypem. Gdy obiekty z którymi masz do czynienia mają mnóstwo pól i setki możliwych konfiguracji, klonowanie ich może okazać się korzystną alternatywą do tworzenia podklas.

Prefabrykowane prototypy

Prefabrykowane prototypy mogą być alternatywą do tworzenia podklas.

Działa to tak: tworzymy zestaw obiektów, każdy skonfigurowany na swój sposób. Jeśli potrzebujesz obiektu, który ma taką samą konfigurację jak już istniejący, klonujesz prototyp — zamiast konstruować nowy obiekt od podstaw.

Analogia do prawdziwego życia

W prawdziwym życiu prototypy stosuje się w testach, zanim rozpoczęta zostanie seryjna produkcja wyrobu. Tu jednak prototypy nie biorą udziału w faktycznej produkcji, lecz pełnią w niej rolę pasywną.

Podział komórki

Podział żywej komórki.

Ponieważ prototypy w przemyśle nie potrafią kopiować siebie, bliższą analogią do prawdziwego życia będzie mitotyczny podział żywej komórki (biologia, pamiętasz?). Po podziale mitotycznym powstaje para identycznych komórek. Pierwotna komórka stanowi prototyp i jednocześnie pełni aktywną rolę w duplikacji.

Struktura

Podstawowa implementacja

Struktura wzorca projektowego PrototypStruktura wzorca projektowego Prototyp
  1. Interfejs Prototyp deklaruje metody klonujące. W większości przypadków jest to pojedyncza metoda klonuj.

  2. Klasa Konkretny Prototyp implementuje metodę klonującą. Oprócz kopiowania danych pierwotnego obiektu do nowo powstałego, metoda ta może również rozwiązywać kwestie związane z klonowaniem obiektów powiązanych, czy też z zależnościami rekursywnymi.

  3. Klient może stworzyć kopię dowolnego obiektu który jest zgodny z interfejsem prototypu.

Implementacja rejestru prototypów

Rejestr prototypówRejestr prototypów
  1. Rejestr prototypów udostępnia łatwy dostęp do często używanych prototypów. Przechowuje zestaw prefabrykowanych obiektów, które są gotowe do skopiowania. Najprostszym rejestrem prototypów będzie tablica asocjacyjna nazwa → prototyp. Jednakże, jeśli potrzebne są bardziej szczegółowe kryteria wyszukiwania, aniżeli tylko nazwa, można zbudować bardziej złożoną wersję rejestru.

Pseudokod

W tym przykładzie, wzorzec Prototyp pozwala tworzyć dokładne kopie figur geometrycznych bez sprzęgania kodu z klasami figur.

Struktura przykładu użycia wzorca projektowego Prototyp

Klonowanie obiektów należących do jednej hierarchii klasowej.

Wszystkie klasy figur są zgodne pod względem interfejsu, który z kolei posiada metodę klonującą. Podklasa może wywołać metodę klonującą klasy-rodzica przed skopiowaniem wartości swoich pól do obiektu wynikowego.

// Bazowy prototyp.
abstract class Shape is
    field X: int
    field Y: int
    field color: string

    // Zwyczajny konstruktor.
    constructor Shape() is
        // ...

    // Konstruktor prototypu. Nowy obiekt jest inicjalizowany
    // wartościami istniejącego obiektu.
    constructor Shape(source: Shape) is
        this()
        this.X = source.X
        this.Y = source.Y
        this.color = source.color

    // Klonowanie zwraca jedną z podklas Shape.
    abstract method clone():Shape


// Konkretny prototyp. Metoda klonująca tworzy świeży obiekt i
// przekazuje go do konstruktora. Dopóki konstruktor nie skończy
// pracy, posiada jako jedyny odniesienie do świeżego klona.
// Gwarantuje to spójność klonowania.
class Rectangle extends Shape is
    field width: int
    field height: int

    constructor Rectangle(source: Rectangle) is
        // Trzeba wywołać konstruktor nadklasy w celu
        // skopiowania pól zdefiniowanych w nadklasie.
        super(source)
        this.width = source.width
        this.height = source.height

    method clone():Shape is
        return new Rectangle(this)


class Circle extends Shape is
    field radius: int

    constructor Circle(source: Circle) is
        super(source)
        this.radius = source.radius

    method clone():Shape is
        return new Circle(this)


// Gdzieś w kodzie klienckim.
class Application is
    field shapes: array of Shape

    constructor Application() is
        Circle circle = new Circle()
        circle.X = 10
        circle.Y = 10
        circle.radius = 20
        shapes.add(circle)

        Circle anotherCircle = circle.clone()
        shapes.add(anotherCircle)
        // Zmienna `anotherCircle` zawiera dokładną kopię
        // obiektu `circle`.

        Rectangle rectangle = new Rectangle()
        rectangle.width = 10
        rectangle.height = 20
        shapes.add(rectangle)

    method businessLogic() is
        // Wzorzec Prototyp rządzi, bo pozwala stworzyć kopię
        // obiektu bez wiedzy o jego typie.
        Array shapesCopy = new Array of Shapes.

        // Na przykład — nie musimy znać dokładnych elementów w
        // tablicy figur (shapes). Wiemy tylko, że wszystkie są
        // figurami. Ale dzięki polimorfizmowi, gdy wywołujemy
        // metodę `klonuj` na figurze, program sprawdza jej
        // klasę i uruchamia odpowiednią metodę klonującą
        // zdefiniowaną w tejże klasie. Dlatego też otrzymujemy
        // odpowiednie obiekty-klony, a nie ogólne obiekty
        // Shape.
        foreach (s in shapes) do
            shapesCopy.add(s.clone())

        // Tablica `shapesCopy` zawiera wierne kopie podklas z
        // tablicy `shape`.

Zastosowanie

Stosuj wzorzec Prototyp gdy chcesz aby twój kod nie był zależny od konkretnej klasy kopiowanego obiektu.

Powyższa sytuacja zdarza się często, gdy twój kod pracuje na obiektach przekazanych z zewnętrznego źródła poprzez jakiś interfejs. Nieznana jest wówczas konkretna klasa takich obiektów.

Wzorzec Prototyp pozwala kodowi klienckiemu na pracę ze wszystkimi obiektami wspierającymi klonowanie za pomocą uogólnionego interfejsu. Interfejs czyni kod kliencki niezależnym od konkretnych klas klonowanych obiektów.

Stosuj ten wzorzec gdy chcesz zredukować ilość podklas różniących się jedynie sposobem inicjalizacji swych obiektów. Ktoś inny bowiem mógł stworzyć takie podklasy tylko w celu tworzenia obiektów o określonej konfiguracji.

Wzorzec Prototyp pozwala korzystać z zestawu prefabrykowanych obiektów w różnorakich konfiguracjach, stanowiących prototypy.

Zamiast tworzyć instancję podklasy zgodnej z jakąś konfiguracją, klient może po prostu wyszukać i sklonować odpowiedni prototyp.

Jak zaimplementować

  1. Stwórz interfejs prototypu i zadeklaruj w nim metodę klonuj. Albo po prostu dodaj tę metodę do wszystkich klas należących do istniejącej hierarchii, o ile taką masz.

  2. Klasa prototypowa musi definiować alternatywny konstruktor, taki, który akceptuje jako argument obiekt swej klasy. Taki konstruktor powinien skopiować cechy przekazanego obiektu do nowo utworzonej instancji. Jeśli modyfikujemy podklasę, musimy wywołać konstruktor klasy-rodzica, aby to nadklasa dokonała klonowania pól zdefiniowanych jako prywatne.

    Jeśli używany przez ciebie język programowania nie wspiera przeciążania metod, możesz zdefiniować specjalną metodę służącą kopiowaniu danych obiektu. Konstruktor jest na nią dogodniejszym miejscem, bo zwraca nowo utworzony obiekt od razu po wywołaniu operatora new.

  3. Metoda klonująca zazwyczaj składa się tylko z jednej linii kodu: wywołanie operatora new z prototypową wersją konstruktora. Pamiętać trzeba, że każda klasa musi wyraźnie nadpisywać metodę klonującą, by stosowała nazwę własnej klasy wraz z operatorem new. W przeciwnym razie metoda klonująca może stworzyć obiekt klasy nadrzędnej.

  4. Opcjonalnie, można stworzyć scentralizowany rejestr prototypów przechowujący katalog najczęściej używanych.

    Możesz zaimplementować rejestr w formie nowej klasy fabrycznej, albo umieścić go w bazowej klasie prototypowej ze statyczną metodą służącą pobieraniu prototypu. Metoda ta powinna szukać prototypu na podstawie określonych przez klienta, przekazanych jej kryteriów. Kryteriami takimi może być albo prosty łańcuch tekstowy, albo też bardziej skomplikowany zestaw parametrów wyszukiwania. Po znalezieniu stosownego prototypu, rejestr powinien sklonować go i zwrócić kopię klientowi.

    Na końcu należy zamienić bezpośrednie wywołania konstruktorów podklas na wywołania metody fabrycznej rejestru prototypów.

Zalety i wady

  • Możesz klonować obiekty bez konieczności sprzęgania ze szczegółami ich konkretnych klas.
  • Możesz pozbyć się wielokrotnie powtarzanego kodu inicjalizacyjnego na rzecz klonowania prefabrykowanych prototypów.
  • Dużo wygodniejsze produkowanie złożonych obiektów.
  • Podejście to stanowi alternatywę do dziedziczenia w przypadku gdy mamy do czynienia z wcześniej zdefiniowanymi konfiguracjami złożonych obiektów.
  • Klonowanie złożonych obiektów, które mają odniesienia cykliczne, może być trudne.

Powiązania z innymi wzorcami

  • Wiele projektów zaczyna się od zastosowania Metody wytwórczej (mniej skomplikowanej i dającej się dostosować poprzez tworzenie podklas). Projekty następnie ewoluują stopniowo w Fabrykę abstrakcyjną, Prototyp lub Budowniczego (bardziej elastyczne, ale i bardziej skomplikowane wzorce).

  • Klasy Fabryka abstrakcyjna często wywodzą się z zestawu Metod wytwórczych, ale można także użyć Prototypu do skomponowania metod w tych klasach.

  • Prototyp może pomóc stworzyć historię, zapisując kopie Poleceń.

  • Projekty intensywnie korzystające ze wzorców Kompozyt i Dekorator mogą skorzystać również na zastosowaniu Prototypu. Zastosowanie tego wzorca pozwala klonować złożone struktury zamiast konstruować je ponownie od zera.

  • Prototyp nie bazuje na dziedziczeniu, więc nie posiada właściwych temu podejściu wad. Z drugiej strony jednak Prototyp wymaga skomplikowanej inicjalizacji klonowanego obiektu. Metoda wytwórcza bazuje na dziedziczeniu, ale nie wymaga etapu inicjalizacji.

  • Czasem Prototyp może być prostszą alternatywą dla Pamiątki. Jest to możliwe jeśli obiekt, którego stan chcesz przechować w historii, jest w miarę nieskomplikowany. Obiekt taki nie powinien też mieć powiązań z zewnętrznymi zasobami, albo muszą one być łatwe do ponownego nawiązania.

  • Fabryki abstrakcyjne, Budowniczych oraz Prototypy można zaimplementować jako Singletony.

Przykłady kodu

Prototyp w języku C# Prototyp w języku C++ Prototyp w języku Go Prototyp w języku Java Prototyp w języku PHP Prototyp w języku Python Prototyp w języku Ruby Prototyp w języku Rust Prototyp w języku Swift Prototyp w języku TypeScript