BackendBit

К списку паттернов

Prototype – паттерн, который позволяет создавать новые объекты путём клонирования существующего объекта (прототипа), а не через конструктор.

UML диаграмма классов паттерна ПрототипПоказывает интерфейс Document с методом copy(), конкретные реализации Report и Invoice, а также класс DocumentProcessor, который использует прототип для создания новых документов.Клонирует прототипдля созданияновых документовDocumentProcessor-templateDocument+createFromTemplate()<<interface>>Document+copy()+getTitle()+setTitle()+getContent()+setContent()Report-author-department+copy()Invoice-client-amount+copy()

Пример

Если мы создаём объект через ключевое слово new, то мы привязываемся к конкретному классу. Есть случаи, когда мы не хотим или вовсе не можем так делать. Например, этот паттерн можно встретить в коде библиотек и фреймворков, которые вынуждены писать обобщённый код. Ведь библиотеке заранее неизвестен код приложения, в котором она будет работать.

Рассмотрим пример. Предположим, мы пишем систему обработки документов. У нас есть базовый интерфейс документа, который должен поддерживать клонирование.

package documents;

public interface Document {
    Document copy();

    String getTitle();

    void setTitle(String title);

    String getContent();

    void setContent(String content);
}
java

Реализуем конкретный тип документа – отчёт.

package documents;

public final class Report implements Document {
    private String title;
    private String content;
    private String author;
    private String department;

    public Report(String title, String content, String author, String department) {
        this.title = title;
        this.content = content;
        this.author = author;
        this.department = department;
    }

    @Override
    public Document copy() {
        return new Report(this.title, this.content, this.author, this.department);
    }

    @Override
    public String getTitle() {
        return title;
    }

    @Override
    public void setTitle(String title) {
        this.title = title;
    }

    @Override
    public String getContent() {
        return content;
    }

    @Override
    public void setContent(String content) {
        this.content = content;
    }

    public String getAuthor() {
        return author;
    }

    public String getDepartment() {
        return department;
    }
}
java

Теперь представим, что у нас есть система, которая работает с документами, не зная их конкретных типов.

package documents;

public final class DocumentProcessor {
    private final Document templateDocument;

    public DocumentProcessor(Document templateDocument) {
        // Процессор не знает, какой конкретный тип документа ему передали.
        // Он не может использовать new Report(...) напрямую.
        this.templateDocument = templateDocument;
    }

    public Document createFromTemplate(String newTitle, String newContent) {
        // Клонируем прототип, чтобы создать новый документ
        Document newDoc = templateDocument.copy();
        newDoc.setTitle(newTitle);
        newDoc.setContent(newContent);
        return newDoc;
    }
}
java

DocumentProcessor заранее неизвестно, какой документ ему нужно будет создавать. Соответственно он не может создавать объекты с помощью ключевого слова new. Вместо этого он клонирует переданный прототип.

// Создаём шаблон отчёта для финансового отдела
Report financeTemplate = new Report(
    "Шаблон отчёта",
    "",
    "Иванов И.И.",
    "Финансовый отдел"
);

DocumentProcessor processor = new DocumentProcessor(financeTemplate);

// Все созданные документы унаследуют автора и отдел от шаблона
Document q1Report = processor.createFromTemplate(
    "Отчёт за Q1 2026",
    "Содержание отчёта..."
);

Document q2Report = processor.createFromTemplate(
    "Отчёт за Q2 2026",
    "Содержание отчёта..."
);
java

Таким образом, библиотека избавляется от необходимости знать о классе, с которым она работает. Поскольку мы используем клонирование, мы можем также переиспользовать состояние объекта-прототипа – все созданные отчёты автоматически получат автора и отдел из шаблона.

Поверхностное и глубокое копирование

При реализации паттерна Prototype важно понимать разницу между поверхностным (shallow) и глубоким (deep) копированием.

Поверхностное копирование создаёт новый объект, но вложенные объекты не копируются – копия ссылается на те же объекты, что и оригинал.

Глубокое копирование создаёт новый объект вместе со всеми вложенными объектами – полностью независимую копию.

Рассмотрим на примере. Допустим, у нас есть заказ со списком позиций.

package orders;

import java.math.BigDecimal;

public record OrderLine(String product, int quantity, BigDecimal price) {}
java
package orders;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public final class ShallowCopyOrder {
    private final String orderId;
    private List<OrderLine> lines;

    public ShallowCopyOrder(String orderId) {
        this.orderId = orderId;
        this.lines = new ArrayList<>();
    }

    public void addLine(OrderLine line) {
        lines.add(line);
    }

    public List<OrderLine> getLines() {
        return Collections.unmodifiableList(lines);
    }

    // Поверхностное копирование – копируется только ссылка на список
    public ShallowCopyOrder shallowCopy(String newOrderId) {
        ShallowCopyOrder copy = new ShallowCopyOrder(newOrderId);
        copy.lines = this.lines; // Опасно! Копия и оригинал ссылаются на один и тот же список
        return copy;
    }
}
java

Проблема поверхностного копирования:

ShallowCopyOrder original = new ShallowCopyOrder("ORD-001");
original.addLine(new OrderLine("Ноутбук", 1, new BigDecimal("999.99")));

ShallowCopyOrder copy = original.shallowCopy("ORD-002");
copy.addLine(new OrderLine("Мышь", 1, new BigDecimal("29.99"))); // Добавляем в копию

System.out.println(original.getLines().size()); // 2 – оригинал тоже изменился!
java

Поскольку original и copy ссылаются на один и тот же список, добавление позиции в копию затрагивает оригинал. Это классическая ошибка при реализации Prototype.

Исправим с помощью глубокого копирования:

package orders;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public final class Order {
    private final String orderId;
    private List<OrderLine> lines;

    public Order(String orderId) {
        this.orderId = orderId;
        this.lines = new ArrayList<>();
    }

    public void addLine(OrderLine line) {
        lines.add(line);
    }

    public List<OrderLine> getLines() {
        return Collections.unmodifiableList(lines);
    }

    // Глубокое копирование – создаём новый список
    public Order copy(String newOrderId) {
        Order copy = new Order(newOrderId);
        copy.lines = new ArrayList<>(this.lines); // Новый список с теми же элементами
        return copy;
    }
}
java

Теперь каждая копия имеет собственный список:

Order original = new Order("ORD-001");
original.addLine(new OrderLine("Ноутбук", 1, new BigDecimal("999.99")));

Order copy = original.copy("ORD-002");
copy.addLine(new OrderLine("Мышь", 1, new BigDecimal("29.99")));

System.out.println(original.getLines().size()); // 1 – оригинал не затронут
System.out.println(copy.getLines().size());     // 2
java

В этом примере мы копируем только список, но не сами объекты OrderLine. Это работает, потому что OrderLine – record с неизменяемыми полями. Если бы OrderLine был изменяемым, пришлось бы копировать и каждую позицию.

Реальные примеры

java.lang.Object.clone()

Стандартный метод clone() в Java – это базовая реализация паттерна Prototype на уровне языка. Любой класс может реализовать интерфейс Cloneable и переопределить метод clone() для поддержки клонирования.

import java.util.Arrays;

public final class IntArray implements Cloneable {
    private int[] data;

    public IntArray(int[] data) {
        this.data = data.clone();
    }

    @Override
    public IntArray clone() {
        try {
            IntArray copy = (IntArray) super.clone();
            copy.data = this.data.clone(); // Глубокое копирование массива
            return copy;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

    public int get(int index) {
        return data[index];
    }

    public void set(int index, int value) {
        data[index] = value;
    }
}
java

Apache Commons Lang – SerializationUtils

Библиотека Apache Commons Lang предоставляет утилиту для глубокого клонирования объектов через сериализацию. Это простой способ получить полную копию объекта без ручного копирования всех вложенных полей.

import org.apache.commons.lang3.SerializationUtils;
import java.io.Serializable;
import java.util.List;

public final class SerializationExample {

    record Customer(String name) implements Serializable {}

    record OrderItem(String product, int quantity) implements Serializable {}

    static final class Order implements Serializable {
        private final String orderId;
        private Customer customer;
        private List<OrderItem> items;

        Order(String orderId, Customer customer, List<OrderItem> items) {
            this.orderId = orderId;
            this.customer = customer;
            this.items = items;
        }
    }

    public static void deepCopyViaSerialization() {
        Customer customer = new Customer("Иванов");
        List<OrderItem> items = List.of(new OrderItem("Ноутбук", 1));

        Order original = new Order("ORD-001", customer, items);
        Order deepCopy = SerializationUtils.clone(original);

        // deepCopy – полностью независимая копия,
        // включая все вложенные объекты Customer и OrderItem
    }
}
java

Этот подход требует, чтобы все клонируемые классы реализовывали интерфейс Serializable, но зато автоматически обеспечивает глубокое копирование всего графа объектов.

http.Request.Clone()

В стандартной библиотеке Go метод Clone() у http.Request – пример паттерна Prototype.

func (r *Request) Clone(ctx context.Context) *Request
go

Метод возвращает глубокую (deep) копию запроса с новым контекстом. Все поля копируются глубоко, кроме Body – для него создаётся только поверхностная (shallow) копия.

Пример использования – кастомный RoundTripper с ретраями. RoundTripper – это интерфейс, который выполняет один HTTP-запрос и возвращает ответ. По контракту http.RoundTripper, реализация не должна модифицировать переданный запрос. Поэтому для каждой попытки создаётся клон:

func (t *RetryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	var resp *http.Response
	var err error

	for attempt := 0; attempt < t.maxRetries; attempt++ {
		// Клонируем запрос для каждой попытки
		reqCopy := req.Clone(req.Context())

		// Можно модифицировать копию, не затрагивая оригинал
		reqCopy.Header.Set("X-Retry-Attempt", strconv.Itoa(attempt))

		resp, err = t.base.RoundTrip(reqCopy)
		if err == nil && resp.StatusCode < 500 {
			return resp, nil
		}
		if resp != nil {
			resp.Body.Close()
		}
	}
	return resp, err
}
go