Prototyp

Wzorce projektowe

  |   12 min czytania

Zastosowanie

Prototyp (ang. Prototype) (wzorzec kreacyjny) ma za zadanie utworzyć kopię macierzystego obiektu zwanego prototypem. Zamiast tworzyć obiekt od nowa używa on istniejącą instancje do wygenerowania jej kopii, która jest zupełnie nowym obiektem, ale o tych samych właściwościach co prototyp. Modyfikacja kopii nie wpływa zatem w żaden sposób na obiekt macierzysty. Użycie wzorca Prototyp uniezależnia system od znajomości sposobu tworzenia obiektów, a także może pozytywnie wpływać na jego wydajność. Wzorzec ten jest standardowym elementem w wielu językach programowania.

Ograniczenia

Kopiowanie skomplikowanych obiektów z wieloma referencjami do innych obiektów może być utrudnione. W takiej sytuacji wiele klas, których instancje zawierają się w klasie obiektu macierzystego muszą implementować metodę klonowania copy.

Użycie

Prototyp stosowany jest tam, gdzie zachodzi potrzeba tworzenia dużej ilości podobnych lub tych samych obiektów, a ich kopiowanie jest wydajniejsze niż tworzenie. Może zostać także wykorzystany wraz z innymi wzorcami kreacyjnymi jak np.: Fabryka abstrakcyjna czy też w procesach przywracania/przechowywania stanu systemu.

Implementacja

Klasy dla których instancje mogą być klonowane implementują metodę copy w której tworzona i zwracana jest kopia bieżącego obiektu na podstawie kopi wszystkich pól obiektu. Zadanie to może zostać oddelegowane także do konstruktora kopiującego. Należy mieć na uwadzę, aby wszystkie klasy (których instancje zawierają się w klasie klonowanej) także implementowały metodę copy lub posiadały konstruktor kopiujący. W przeciwnym wypadku w klasie macierzystej należy rekurencyjnie pobrać wszystkie wartości prymitywne.

Prototyp diagram

Poniższy listing przedstawia implementacja wzorca Prototyp dla klasy Prototype1 składającej się z prymitów - płytka kopia (shallow copy) oraz dla klasy Prototype2 składającej się z obiektów złożonych - głęboka kopia (deep copy).

public class Prototype1 extends Prototype {

    private double price;
    //other primitive fields

    public Prototype1() {
        this(0);
    }

    public Prototype1(int price) {
        super();
        this.price = price;
    }

    public Prototype1(Prototype1 prototype) {
        super(prototype);
        if(prototype != null) {
            this.price = prototype.price;
            //do the same for all fields
        }
    }

    @Override
    public Prototype copy() {
        //shallow copy
        return new Prototype1(this);
    }

    @Override
    public void action() {
        //do specific action for Prototype1
    }

    //some other methods
}

public class Prototype2 extends Prototype {

    private int value;
    private Complex complex; //has only int and boolean fields
    //other object and primitive types

    public Prototype2() {
        this(0, new Complex(0, false));
    }

    public Prototype2(int value, Complex complex) {
        super();
        this.value = value;
        this.complex = complex;
    }

    public Prototype2(Prototype2 prototype) {
        super(prototype);
        if(prototype != null) {
            this.value = prototype.value;
            //copy complex object by getting all primitive types or provide copy method/copy constructor
            this.complex = new Complex(prototype.complex.getNumber(), prototype.complex.getAvailability()); 
            //do the same for all fields
        }
    }

    @Override
    public Prototype copy() {
        //deep copy
        return new Prototype2(this);
    }

    @Override
    public void action() {
        //do specific action for Prototype2
    }

    //some other methods
}

public abstract class Prototype {

    //some fields
    //some common methods

    public Prototype() {
        //initialize fields  
    }

    public Prototype(Prototype prototype) {
        //copy all fields from prototype into this instance
    } 

    //in Java clone is abstract method of Object class so no need copy extra method
    public abstract Prototype copy();
    public abstract void action();
}

Na podstawie istniejących obiektów (prototypów) klient generuje ich kopie. Następnie na całej kolekcji klonów wykonuje operacje.

//create prototype to copy
Prototype1 prototype1 = new Prototype1(10.0);
Prototype2 prototype2 = new Prototype2(5, new Complex(3, true));

//copy prototypes to save time and resources
List<Prototype> clones = new ArrayList<>();
Prototype1 clone1 = prototype1.copy();
clones.add(clone1);
Prototype2 clone2 = prototype2.copy();
clones.add(clone2);
//do more clones

//do some action for every copies
for(Prototype copy : clones)
    copy.action();

Przykład

Aplikacja MathFigures pomaga w nauce geometrii i figur geometrycznych. Pozwala m.in. na rysowanie figur na tablicy, a następnie na wyliczenie dla nich pola powierzchni oraz obwodu. Rezultaty te mogą być porównane z wyliczeniami użytkownika dzięki czemu sprawdza on swoją wiedzę. Użytkownik tworząc nowe figury geometryczne często kopiuje już istniejące figury, a następnie modyfikuje je wg swojego uznania. Ze względu na wymóg klonowanie istniejących figur w projekcie został zaimplementowany wzorzec Prototyp.

public class Circle extends Figure {

    private int x, y, radius;

    public Circle() {
        this(0, 0, 0);
    }

    public Circle(int x, int y, int radius) {
        super();
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    public Circle(Circle prototype) {
        super(prototype);
        if(prototype != null) {
            this.x = prototype.x;
            this.y = prototype.y;
            this.radius = prototype.radius;
        }
    }

    @Override
    public Figure copy() {
        //shallow copy
        return new Circle(this);
    }

    @Override
    public void draw() {
        //draw Circle on the board in own way
    }

    @Override
    public double area() {
        return Math.PI * Math.pow(radius);
    }

    @Override
    public double perimeter() {
        return 4 * Math.PI * radius;
    }
}

public class Triangle {

    private Point point1, point2, point3;

    public Triangle() {
        this(new Point(0, 0), new Point(0, 0), new Point(0, 0));
    }

    public Triangle(Point point1, Point point2, Point point3) {
        super();
        this.point1 = point1;
        this.point2 = point2;
        this.point3 = point3;
    }

    public Triangle(Triangle prototype) {
        super(prototype);
        if(prototype != null) {
            this.point1 = new Point(prototype.point1.getX(), prototype.point1.getY());
            this.point2 = new Point(prototype.point2.getX(), prototype.point2.getY());
            this.radius = new Point(prototype.point3.getX(), prototype.point3.getY());
        }
    }

    @Override
    public Figure copy() {
        //deep copy
        return new Triangle(this);
    }

    @Override
    public void draw() {
        //draw Triangle on the board in own way
    }

    @Override
    public double area() {
        double p = perimeter() / 2;
        double a = getSideLength(point1, point2);
        double b = getSideLength(point1, point3);
        double c = getSideLength(point2, point3);
        return Math.sqrt(p*(p-a)*(p-b)*(p-c));
    }

    @Override
    public double perimeter() {
        double a = getSideLength(point1, point2);
        double b = getSideLength(point1, point3);
        double c = getSideLength(point2, point3);
        return a + b + c;
    }

    private double getSideLength(Point point1, Point point2) {
        double size = Math.pow(point1.getX() - point2.getX()) + Math.pow(point1.getY() - point2.getY());
        return Math.sqrt(size);
    }
}

//other figures like rectangle

public abstract class Figure {

    private int color;
    private int borderColor;

    public Figure() {
        this.color = Color.WHITE;
        this.borderColor = Color.BLACK;
    }

    public Figure(int color, int borderColor) {
        this.color = color;
        this.borderColor = borderColor;
    }

    public Figure(Figure prototype) {
        this.color = prototype.color;
        this.borderColor = prototype.borderColor;
    }

    public abstract Prototype copy();
    public abstract void draw();
    public abstract double area();
    public abstract double perimeter();
}

Użytkownik tworzy w całości kilka figur i na podstawie ich kopii generuje kolejne oraz sprawdza swoje obliczenia z faktycznymi wynikami.

//new empty board initialize
List<Figure> figures = new ArrayList();

//user creates some figures
Circle prototypeCircle = new Circle(10, 5, 4);
prototypeCircle.draw();
figures.add(prototypeCircle);

Triangle prototypeTriangle = new Triangle(new Point(1, 1), new Point(1, 5), new Point(5, 1));
prototypeTriangle.draw();
figures.add(prototypeTriangle);

//after creation basic giures, user just copy them instead of creat new ones from the beginning
//some of them are modifed
Circle copy1 = prototypeCircle.copy();
copy1.setColor(Color.BLUE);
copy.setRadius(2);
figures.add(copy1);

Triangle copy2 = prototypeTriangle.copy();
copy2.setColor(Color.GREEN);
triangle.setPoint1(new Point(5, 5));
figures.add(copy2);

//user calculates themself area and perimeter of the figures and check them with real results
//calculate area and delimeter for all figures on the board
for(Figure figure : figures) {
    double area = figures.area();
    double perimeter = figures.perimeter();
    //check the answer
}

Biblioteki

Każda klasa implementująca interfejs Cloneable oraz rozszerzająca metodę copy klasy Object (ze standardowego pakietu Java) jest przykładem implementacji wzorca Prototyp jednakże ze względu na narzucane ograniczenia takie rozwiązanie jest odradzane. Jako alternatywe podaje się BeanUtils.cloneBean oraz SerializationUtils.clone.