Autumn SALE

Прототип

Також відомий як: Клон, Prototype

Суть патерна

Прототип — це породжувальний патерн проектування, що дає змогу копіювати об’єкти, не вдаючись у подробиці їхньої реалізації.

Патерн Прототип

Проблема

У вас є об’єкт, який потрібно скопіювати. Як це зробити? Потрібно створити порожній об’єкт того самого класу, а потім по черзі копіювати значення всіх полів зі старого об’єкта до нового.

Чудово! Проте є нюанс. Не кожен об’єкт вдасться скопіювати у такий спосіб, адже частина його стану може бути приватною, а значить — недоступною для решти коду програми.

Приклад невдалого копіювання ззовні

Копіювання «ззовні» не завжди можливе на практиці.

Є й інша проблема. Код, що копіює, стане залежним від класів об’єктів, які він копіює. Адже, щоб перебрати усі поля об’єкта, потрібно прив’язатися до його класу. Тому ви не зможете копіювати об’єкти, знаючи тільки їхні інтерфейси, але не їхні конкретні класи.

Рішення

Патерн Прототип доручає процес копіювання самим об’єктам, які треба скопіювати. Він вводить загальний інтерфейс для всіх об’єктів, що підтримують клонування. Це дозволяє копіювати об’єкти, не прив’язуючись до їхніх конкретних класів. Зазвичай такий інтерфейс має всього один метод — clone.

Реалізація цього методу в різних класах дуже схожа. Метод створює новий об’єкт поточного класу й копіює в нього значення всіх полів власного об’єкта. Таким чином можна скопіювати навіть приватні поля, оскільки більшість мов програмування дозволяє отримати доступ до приватних полів будь-якого об’єкта поточного класу.

Об’єкт, який копіюють, називається прототипом (звідси і назва патерна). Коли об’єкти програми містять сотні полів і тисячі можливих конфігурацій, прототипи можуть слугувати своєрідною альтернативою створенню підкласів.

Попередньо заготовлені прототипи

Попередньо заготовлені прототипи можуть стати заміною підкласів.

У цьому випадку всі можливі прототипи готуються і налаштовуються на етапі ініціалізації програми. Потім, коли програмі буде потрібний новий об’єкт, вона створить копію з попередньо заготовленого прототипа.

Аналогія з життя

У промисловому виробництві прототипи створюються перед виготовленням основної партії продуктів для проведення різноманітних випробувань. При цьому прототип не бере участі в подальшому виробництві, відіграючи пасивну роль.

Приклад поділу клітини

Приклад поділу клітини.

Виробничий прототип не створює копію самого себе, тому більш наближений до патерна приклад — це поділ клітин. Після мітозного поділу клітин утворюються дві абсолютно ідентичні клітини. Материнська клітина відіграє роль прототипу, беручи активну участь у створенні нового об’єкта.

Структура

Базова реалізація

Структура класів патерна ПрототипСтруктура класів патерна Прототип
  1. Інтерфейс прототипів описує операції клонування. Для більшості випадків — це єдиний метод clone.

  2. Конкретний прототип реалізує операцію клонування самого себе. Крім звичайного копіювання значень усіх полів, тут можуть бути приховані різноманітні складнощі, про які клієнту не потрібно знати. Наприклад, клонування пов’язаних об’єктів, розплутування рекурсивних залежностей та інше.

  3. Клієнт створює копію об’єкта, звертаючись до нього через загальний інтерфейс прототипів.

Реалізація зі спільним сховищем прототипів

Варіант Прототипу зі спільним сховищем прототипівВаріант Прототипу зі спільним сховищем прототипів
  1. Сховище прототипів полегшує доступ до часто використовуваних прототипів, зберігаючи попередньо створений набір еталонних, готових до копіювання об’єктів. Найпростіше сховище може бути побудовано за допомогою хеш-таблиці виду ім'я-прототипупрототип. Для полегшення пошуку прототипи можна маркувати ще й за іншими критеріями, а не тільки за умовним іменем.

Псевдокод

У цьому прикладі Прототип дозволяє робити точні копії об’єктів геометричних фігур без прив’язки до їхніх класів.

Структура класів прикладу патерна Прототип

Приклад клонування ієрархії геометричних фігур.

Кожна фігура реалізує інтерфейс клонування і надає метод для відтворення самої себе. Підкласи використовують батьківський метод клонування, а потім копіюють власні поля до створеного об’єкта.

// Базовий прототип.
abstract class Shape is
    field X: int
    field Y: int
    field color: string

    // Звичайний конструктор.
    constructor Shape() is
        // ...

    // Конструктор прототипа.
    constructor Shape(source: Shape) is
        this()
        this.X = source.X
        this.Y = source.Y
        this.color = source.color

    // Результатом операції клонування завжди буде об'єкт з
    // ієрархії класів Shape.
    abstract method clone(): Shape


// Конкретний прототип. Метод клонування створює новий об'єкт
// поточного класу, передаючи до конструктора посилання на
// власний об'єкт. Завдяки цьому, клонування виходить
// атомарним — доки не виконається конструктор, нового об'єкта
// ще не існує. Але як тільки конструктор завершено, ми
// отримаємо завершений об'єкт-клон, а не порожній об'єкт, який
// потрібно ще заповнити.
class Rectangle extends Shape is
    field width: int
    field height: int

    constructor Rectangle(source: Rectangle) is
        // Виклик батьківського конструктора потрібен, щоб
        // скопіювати потенційні приватні поля, оголошені в
        // батьківському класі.
        super(source)
        this.width = source.width
        this.height = source.height

    method clone(): Shape is
        return new Rectangle(this)


class Circle extends Shape is
    field radius: int

    constructor Circle(source: Circle) is
        super(source)
        this.radius = source.radius

    method clone(): Shape is
        return new Circle(this)


// Десь у клієнтському програмному коді.
class Application is
    field shapes: array of Shape

    constructor Application() is
        Circle circle = new Circle()
        circle.X = 10
        circle.Y = 10
        circle.radius = 20
        shapes.add(circle)

        Circle anotherCircle = circle.clone()
        shapes.add(anotherCircle)
        // anotherCircle буде містити точну копію circle.

        Rectangle rectangle = new Rectangle()
        rectangle.width = 10
        rectangle.height = 20
        shapes.add(rectangle)

    method businessLogic() is
        // Неочевидний плюс Прототипу в тому, що ви можете
        // клонувати набір об'єктів, не знаючи їхніх конкретних
        // класів.
        Array shapesCopy = new Array of Shapes.

        // Наприклад, ми не знаємо, які конкретно об'єкти
        // знаходяться всередині масиву shapes так як його
        // оголошено з типом Shape. Але завдяки поліморфізму, ми
        // можемо клонувати усі об'єкти «наосліп». Буде виконано
        // метод clone того класу, яким є цей об'єкт.
        foreach (s in shapes) do
            shapesCopy.add(s.clone())

        // Змінна shapesCopy буде містити точні копії елементів
        // масиву shapes.

Застосування

Коли ваш код не повинен залежати від класів об’єктів, призначених для копіювання.

Таке часто буває, якщо ваш код працює з об’єктами, поданими ззовні через який-небудь загальний інтерфейс. Ви не зможете прив’язатися до їхніх класів, навіть якби захотіли, тому що конкретні класи об’єктів невідомі.

Патерн Прототип надає клієнту загальний інтерфейс для роботи з усіма прототипами. Клієнту не потрібно залежати від усіх класів об’єктів, призначених для копіювання, а тільки від інтерфейсу клонування.

Коли ви маєте безліч підкласів, які відрізняються початковими значеннями полів. Хтось міг створити усі ці класи для того, щоб мати легкий спосіб породжувати об’єкти певної конфігурації.

Патерн Прототип пропонує використовувати набір прототипів замість створення підкласів для опису популярних конфігурацій об’єктів.

Таким чином, замість породження об’єктів з підкласів ви копіюватимете існуючі об’єкти-прототипи, внутрішній стан яких вже налаштовано. Це дозволить уникнути вибухоподібного зростання кількості класів програми й зменшити її складність.

Кроки реалізації

  1. Створіть інтерфейс прототипів з єдиним методом clone. Якщо у вас вже є ієрархія продуктів, метод клонування можна оголосити в кожному з її класів.

  2. Додайте до класів майбутніх прототипів альтернативний конструктор, що приймає в якості аргументу об’єкт поточного класу. Спочатку цей конструктор повинен скопіювати значення всіх полів поданого об’єкта, оголошених в рамках поточного класу. Потім — передати виконання батьківському конструктору, щоб той потурбувався про поля, оголошені в суперкласі.

    Якщо мова програмування, яку ви використовуєте, не підтримує перевантаження методів, тоді вам не вдасться створити декілька версій конструктора. В цьому випадку копіювання значень можна проводити в іншому методі, спеціально створеному для цих цілей. Конструктор є зручнішим, тому що дозволяє клонувати об’єкт за один виклик.

  3. Зазвичай метод клонування складається з одного рядка, а саме виклику оператора new з конструктором прототипу. Усі класи, що підтримують клонування, повинні явно визначити метод clone для того, щоб вказати власний клас з оператором new. Інакше результатом клонування стане об’єкт батьківського класу.

  4. На додачу можете створити центральне сховище прототипів. У ньому зручно зберігати варіації об’єктів, можливо, навіть одного класу, але по-різному налаштованих.

    Ви можете розмістити це сховище або у новому фабричному класі, або у фабричному методі базового класу прототипів. Такий фабричний метод, керуючись вхідними аргументами, повинен шукати відповідний екземпляр у сховищі прототипів, а потім викликати його метод клонування і повертати отриманий об’єкт.

    Нарешті, потрібно позбутися прямих викликів конструкторів об’єктів, замінивши їх викликами фабричного методу сховища прототипів.

Переваги та недоліки

  • Дозволяє клонувати об’єкти без прив’язки до їхніх конкретних класів.
  • Менша кількість повторювань коду ініціалізації об’єктів.
  • Прискорює створення об’єктів.
  • Альтернатива створенню підкласів під час конструювання складних об’єктів.
  • Складно клонувати складові об’єкти, що мають посилання на інші об’єкти.

Відносини з іншими патернами

Приклади реалізації патерна

Прототип на C# Прототип на C++ Прототип на Go Прототип на Java Прототип на PHP Прототип на Python Прототип на Ruby Прототип на Rust Прототип на Swift Прототип на TypeScript