Прототип
Суть патерна
Прототип — це породжувальний патерн проектування, що дає змогу копіювати об’єкти, не вдаючись у подробиці їхньої реалізації.
Проблема
У вас є об’єкт, який потрібно скопіювати. Як це зробити? Потрібно створити порожній об’єкт того самого класу, а потім по черзі копіювати значення всіх полів зі старого об’єкта до нового.
Чудово! Проте є нюанс. Не кожен об’єкт вдасться скопіювати у такий спосіб, адже частина його стану може бути приватною, а значить — недоступною для решти коду програми.
Є й інша проблема. Код, що копіює, стане залежним від класів об’єктів, які він копіює. Адже, щоб перебрати усі поля об’єкта, потрібно прив’язатися до його класу. Тому ви не зможете копіювати об’єкти, знаючи тільки їхні інтерфейси, але не їхні конкретні класи.
Рішення
Патерн Прототип доручає процес копіювання самим об’єктам, які треба скопіювати. Він вводить загальний інтерфейс для всіх об’єктів, що підтримують клонування. Це дозволяє копіювати об’єкти, не прив’язуючись до їхніх конкретних класів. Зазвичай такий інтерфейс має всього один метод — clone
.
Реалізація цього методу в різних класах дуже схожа. Метод створює новий об’єкт поточного класу й копіює в нього значення всіх полів власного об’єкта. Таким чином можна скопіювати навіть приватні поля, оскільки більшість мов програмування дозволяє отримати доступ до приватних полів будь-якого об’єкта поточного класу.
Об’єкт, який копіюють, називається прототипом (звідси і назва патерна). Коли об’єкти програми містять сотні полів і тисячі можливих конфігурацій, прототипи можуть слугувати своєрідною альтернативою створенню підкласів.
У цьому випадку всі можливі прототипи готуються і налаштовуються на етапі ініціалізації програми. Потім, коли програмі буде потрібний новий об’єкт, вона створить копію з попередньо заготовленого прототипа.
Аналогія з життя
У промисловому виробництві прототипи створюються перед виготовленням основної партії продуктів для проведення різноманітних випробувань. При цьому прототип не бере участі в подальшому виробництві, відіграючи пасивну роль.
Виробничий прототип не створює копію самого себе, тому більш наближений до патерна приклад — це поділ клітин. Після мітозного поділу клітин утворюються дві абсолютно ідентичні клітини. Материнська клітина відіграє роль прототипу, беручи активну участь у створенні нового об’єкта.
Структура
Базова реалізація
-
Інтерфейс прототипів описує операції клонування. Для більшості випадків — це єдиний метод
clone
. -
Конкретний прототип реалізує операцію клонування самого себе. Крім звичайного копіювання значень усіх полів, тут можуть бути приховані різноманітні складнощі, про які клієнту не потрібно знати. Наприклад, клонування пов’язаних об’єктів, розплутування рекурсивних залежностей та інше.
-
Клієнт створює копію об’єкта, звертаючись до нього через загальний інтерфейс прототипів.
Реалізація зі спільним сховищем прототипів
-
Сховище прототипів полегшує доступ до часто використовуваних прототипів, зберігаючи попередньо створений набір еталонних, готових до копіювання об’єктів. Найпростіше сховище може бути побудовано за допомогою хеш-таблиці виду
ім'я-прототипу
→прототип
. Для полегшення пошуку прототипи можна маркувати ще й за іншими критеріями, а не тільки за умовним іменем.
Псевдокод
У цьому прикладі Прототип дозволяє робити точні копії об’єктів геометричних фігур без прив’язки до їхніх класів.
Кожна фігура реалізує інтерфейс клонування і надає метод для відтворення самої себе. Підкласи використовують батьківський метод клонування, а потім копіюють власні поля до створеного об’єкта.
Застосування
Коли ваш код не повинен залежати від класів об’єктів, призначених для копіювання.
Таке часто буває, якщо ваш код працює з об’єктами, поданими ззовні через який-небудь загальний інтерфейс. Ви не зможете прив’язатися до їхніх класів, навіть якби захотіли, тому що конкретні класи об’єктів невідомі.
Патерн Прототип надає клієнту загальний інтерфейс для роботи з усіма прототипами. Клієнту не потрібно залежати від усіх класів об’єктів, призначених для копіювання, а тільки від інтерфейсу клонування.
Коли ви маєте безліч підкласів, які відрізняються початковими значеннями полів. Хтось міг створити усі ці класи для того, щоб мати легкий спосіб породжувати об’єкти певної конфігурації.
Патерн Прототип пропонує використовувати набір прототипів замість створення підкласів для опису популярних конфігурацій об’єктів.
Таким чином, замість породження об’єктів з підкласів ви копіюватимете існуючі об’єкти-прототипи, внутрішній стан яких вже налаштовано. Це дозволить уникнути вибухоподібного зростання кількості класів програми й зменшити її складність.
Кроки реалізації
-
Створіть інтерфейс прототипів з єдиним методом
clone
. Якщо у вас вже є ієрархія продуктів, метод клонування можна оголосити в кожному з її класів. -
Додайте до класів майбутніх прототипів альтернативний конструктор, що приймає в якості аргументу об’єкт поточного класу. Спочатку цей конструктор повинен скопіювати значення всіх полів поданого об’єкта, оголошених в рамках поточного класу. Потім — передати виконання батьківському конструктору, щоб той потурбувався про поля, оголошені в суперкласі.
Якщо мова програмування, яку ви використовуєте, не підтримує перевантаження методів, тоді вам не вдасться створити декілька версій конструктора. В цьому випадку копіювання значень можна проводити в іншому методі, спеціально створеному для цих цілей. Конструктор є зручнішим, тому що дозволяє клонувати об’єкт за один виклик.
-
Зазвичай метод клонування складається з одного рядка, а саме виклику оператора
new
з конструктором прототипу. Усі класи, що підтримують клонування, повинні явно визначити методclone
для того, щоб вказати власний клас з операторомnew
. Інакше результатом клонування стане об’єкт батьківського класу. -
На додачу можете створити центральне сховище прототипів. У ньому зручно зберігати варіації об’єктів, можливо, навіть одного класу, але по-різному налаштованих.
Ви можете розмістити це сховище або у новому фабричному класі, або у фабричному методі базового класу прототипів. Такий фабричний метод, керуючись вхідними аргументами, повинен шукати відповідний екземпляр у сховищі прототипів, а потім викликати його метод клонування і повертати отриманий об’єкт.
Нарешті, потрібно позбутися прямих викликів конструкторів об’єктів, замінивши їх викликами фабричного методу сховища прототипів.
Переваги та недоліки
- Дозволяє клонувати об’єкти без прив’язки до їхніх конкретних класів.
- Менша кількість повторювань коду ініціалізації об’єктів.
- Прискорює створення об’єктів.
- Альтернатива створенню підкласів під час конструювання складних об’єктів.
- Складно клонувати складові об’єкти, що мають посилання на інші об’єкти.
Відносини з іншими патернами
-
Багато архітектур починаються із застосування Фабричного методу (простішого та більш розширюваного за допомогою підкласів) та еволюціонують у бік Абстрактної фабрики, Прототипу або Будівельника (гнучкіших, але й складніших).
-
Класи Абстрактної фабрики найчастіше реалізуються за допомогою Фабричного методу, хоча вони можуть бути побудовані і на основі Прототипу.
-
Якщо Команду потрібно копіювати перед вставкою в історію виконаних команд, вам може допомогти Прототип.
-
Архітектура, побудована на Компонувальниках та Декораторах, часто може поліпшуватися за рахунок впровадження Прототипу. Він дозволяє клонувати складні структури об’єктів, а не збирати їх заново.
-
Прототип не спирається на спадкування, але йому потрібна складна операція ініціалізації. Фабричний метод, навпаки, побудований на спадкуванні, але не вимагає складної ініціалізації.
-
Знімок іноді можна замінити Прототипом, якщо об’єкт, чий стан потрібно зберігати в історії, досить простий, не має посилань на зовнішні ресурси або їх можна легко відновити.
-
Абстрактна фабрика, Будівельник та Прототип можуть реалізовуватися за допомогою Одинака.