Prototype (Прототип)
Prototype – паттерн, который позволяет создавать новые объекты путём клонирования существующего объекта (прототипа), а не через конструктор.
Пример
Если мы создаём объект через ключевое слово 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;
}
}javaDocumentProcessor заранее неизвестно, какой документ ему нужно будет создавать. Соответственно он не может создавать объекты с помощью ключевого слова 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) {}javapackage 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()); // 2javaВ этом примере мы копируем только список, но не сами объекты 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;
}
}javaApache 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) *RequestgoМетод возвращает глубокую (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