Prototype
Prototype позволяет копировать существующие объекты, не делая код зависимым от их классов. Нередко возникает ситуация, когда некоторые поля класса могут быть приватными, и их значения получить нельзя. Здесь в игру вступает Прототип - его удобство заключается в том, что объекты можно копировать, не вдаваясь в подробности их реализации.
Является паттерном порождающего типа.
Для чего используется?
Часто в разработке возникает ситуация, когда требуется создать множество объектов, практически не имеющих отличий. В таком случае целесообразно создать эталонный объект, называемый прототипом, и клонировать его требуемое количество раз.
Пример из разработки игр - не требуется каждый раз заново отрисовывать модель всех камней при каждом движении игрового персонажа, достаточно создать эталонный экземпляр камня и обновлять положение всех остальных клонированных объектов, отталкиваясь от этого экземпляра.
Когда удобно применять:
- Класс объекта определяется динамически прямо во время выполнения программы;
- Не требуется выстраивать систему наследования создателя объекта;
- Создание объекта дороже и сложнее копирования существующего, и в объект не нужно вносить больших изменений;
- Нужно создать объекты с похожей структурой, но разными данными.
Варианты кейсов использования:
- Генерация тестовых данных;
- Автоматизация развёртывания серверов и сервисов;
- Быстрое масштабирование инфраструктуры.
Какова механика реализации?
Созданием копий занимаются сами копируемые объекты. Для всех поддерживающих клонирование объектов реализуется общий интерфейс, не зависящий от основного класса иерархически.
Создается объект текущего класса, и в него копируются значения всех полей - даже приватные поля, данные которых могут быть недоступны в иных случаях! Изначально настроенный прототип, другими словами, эталон объекта, затем можно копировать требуемое количество раз.
Структура и отношения
Prototype
└── clone(): Prototype
ConcretePrototype1
├── field1: string
└── clone(): Prototype
ConcretePrototype2
├── field1: string
└── clone(): Prototype
Client
└── operation(prototype: Prototype): void
Пример реализации
// Базовый прототип
class Shape {
constructor() {
this.x = 0;
this.y = 0;
this.color = 'black';
}
clone() {
throw new Error('Метод clone должен быть переопределен');
}
render() {
throw new Error('Метод render должен быть переопределен');
}
}
// Конкретные прототипы
class Circle extends Shape {
constructor(radius = 1) {
super();
this.radius = radius;
}
clone() {
const cloned = new Circle(this.radius);
cloned.x = this.x;
cloned.y = this.y;
cloned.color = this.color;
return cloned;
}
render() {
return `Отрисовка круга: радиус=${this.radius}, цвет=${this.color}, позиция=(${this.x}, ${this.y})`;
}
}
class Rectangle extends Shape {
constructor(width = 1, height = 1) {
super();
this.width = width;
this.height = height;
}
clone() {
const cloned = new Rectangle(this.width, this.height);
cloned.x = this.x;
cloned.y = this.y;
cloned.color = this.color;
return cloned;
}
render() {
return `Отрисовка прямоугольника: размер=${this.width}x${this.height}, цвет=${this.color}, позиция=(${this.x}, ${this.y})`;
}
}
// Менеджер прототипов
class ShapeManager {
constructor() {
this.shapes = {};
}
addShape(key, shape) {
this.shapes[key] = shape;
}
getShape(key) {
return this.shapes[key].clone();
}
}
// Использование
const manager = new ShapeManager();
// Создаем прототипы
const circlePrototype = new Circle(5);
circlePrototype.color = 'red';
circlePrototype.x = 10;
circlePrototype.y = 20;
const rectanglePrototype = new Rectangle(10, 5);
rectanglePrototype.color = 'blue';
rectanglePrototype.x = 30;
rectanglePrototype.y = 40;
// Регистрируем прототипы
manager.addShape('red-circle', circlePrototype);
manager.addShape('blue-rectangle', rectanglePrototype);
// Создаем копии
const circle1 = manager.getShape('red-circle');
const circle2 = manager.getShape('red-circle');
const rectangle1 = manager.getShape('blue-rectangle');
// Изменяем копии независимо
circle1.x = 100;
circle2.y = 200;
rectangle1.color = 'green';
console.log(circle1.render());
console.log(circle2.render());
console.log(rectangle1.render());
import copy
class Shape:
def __init__(self):
self._objects = {}
def register_object(self, name, obj):
self.objects[name] = obj
def clone(self, name):
if name in self.objects:
return copy.deepcopy(self._objects[name])
else:
return None
obj1 = 'circle'
obj2 = 'rectangle'
prototype = Shape()
prototype.register_object('obj1', obj1)
prototype.register_object('obj2', obj2)
cloned_obj1 = prototype.clone('obj1')
cloned_obj2 = prototype.clone('obj2')
print(cloned_obj1, cloned_obj2)
// Интерфейс прототипа
interface Cloneable {
clone(): Cloneable;
}
// Базовый класс документа
abstract class Document implements Cloneable {
protected title: string;
protected content: string;
protected author: string;
constructor(title: string, content: string, author: string) {
this.title = title;
this.content = content;
this.author = author;
}
abstract clone(): Document;
abstract render(): string;
setTitle(title: string): void {
this.title = title;
}
setContent(content: string): void {
this.content = content;
}
}
// Конкретные типы документов
class TextDocument extends Document {
private fontSize: number;
constructor(title: string, content: string, author: string, fontSize: number = 12) {
super(title, content, author);
this.fontSize = fontSize;
}
clone(): TextDocument {
const cloned = new TextDocument(this.title, this.content, this.author, this.fontSize);
return cloned;
}
render(): string {
return `Текстовый документ: "${this.title}" (${this.fontSize}px) - ${this.author}`;
}
setFontSize(size: number): void {
this.fontSize = size;
}
}
class SpreadsheetDocument extends Document {
private rows: number;
private columns: number;
constructor(title: string, content: string, author: string, rows: number = 10, columns: number = 5) {
super(title, content, author);
this.rows = rows;
this.columns = columns;
}
clone(): SpreadsheetDocument {
const cloned = new SpreadsheetDocument(this.title, this.content, this.author, this.rows, this.columns);
return cloned;
}
render(): string {
return `Таблица: "${this.title}" (${this.rows}x${this.columns}) - ${this.author}`;
}
setDimensions(rows: number, columns: number): void {
this.rows = rows;
this.columns = columns;
}
}
// Использование
const textTemplate = new TextDocument('Шаблон отчета', 'Содержимое отчета...', 'Администратор', 14);
const spreadsheetTemplate = new SpreadsheetDocument('Шаблон таблицы', 'Данные таблицы...', 'Администратор', 20, 10);
// Создаем копии
const report1 = textTemplate.clone();
const report2 = textTemplate.clone();
const table1 = spreadsheetTemplate.clone();
// Настраиваем копии
report1.setTitle('Отчет за январь');
report1.setContent('Данные за январь...');
report1.setFontSize(16);
report2.setTitle('Отчет за февраль');
report2.setContent('Данные за февраль...');
table1.setTitle('Продажи Q1');
table1.setDimensions(15, 8);
console.log(report1.render());
console.log(report2.render());
console.log(table1.render());
Преимущества
- Объекты клонируются в изоляции от конкретных классов - код отработает вне зависимости от класса-инициализатора;
- Клонирование объектов требует меньших ресурсов и времени, чем их создание;
- Можно в любой момент создать нужное количество объектов, копируя существующие.
Недостатки
- В зависимости от объекта, копирование может быть ресурсозатратным. Некоторые объекты содержат крупные, "тяжелые" данные;
- Есть вероятность клонирования защищенных данных с угрозой для их безопасности;
- Составные объекты, имеющие ссылки на другие объекты, сложно и запутанно копировать.
Отличия от других паттернов
- Factory Method — его прямой конкурент. Фабричный метод построен на наследовании, но не требует сложной инициализации - в то время как Прототип, напротив, не требует наследования, но объект-Прототип нужно правильно настроить изначально.
- Abstract Factory — похож на Прототип, но создает интерфейсы для продуктов, составляющих семейства.
Реальный пример
// Прототип для создания пользовательских интерфейсов
class UIComponent {
constructor(type, props = {}) {
this.type = type;
this.props = { ...props };
this.children = [];
}
clone() {
const cloned = new UIComponent(this.type, this.props);
cloned.children = [...this.children];
return cloned;
}
addChild(child) {
this.children.push(child);
return this;
}
render() {
return `<${this.type} ${Object.entries(this.props).map(([k, v]) => `${k}="${v}"`).join(' ')}>${this.children.map(child => child.render()).join('')}</${this.type}>`;
}
}
// Создаем прототипы компонентов
const buttonPrototype = new UIComponent('button', { class: 'btn', type: 'button' });
const inputPrototype = new UIComponent('input', { type: 'text', class: 'form-control' });
const divPrototype = new UIComponent('div', { class: 'container' });
// Создаем копии и настраиваем их
const submitButton = buttonPrototype.clone();
submitButton.props.value = 'Отправить';
submitButton.props.class = 'btn btn-primary';
const cancelButton = buttonPrototype.clone();
cancelButton.props.value = 'Отмена';
cancelButton.props.class = 'btn btn-secondary';
const form = divPrototype.clone();
form.props.class = 'form-group';
const nameInput = inputPrototype.clone();
nameInput.props.placeholder = 'Введите имя';
nameInput.props.name = 'name';
const emailInput = inputPrototype.clone();
emailInput.props.placeholder = 'Введите email';
emailInput.props.name = 'email';
emailInput.props.type = 'email';
// Собираем форму
form.addChild(nameInput);
form.addChild(emailInput);
form.addChild(submitButton);
form.addChild(cancelButton);
console.log(form.render());
Prototype — это элегантный способ создания объектов путем клонирования!