Перейти к содержанию

Prototype (Прототип)

Категория: порождающий паттерн.

Проблема

Нужно создавать объекты как копии уже настроенного экземпляра, но клиент не должен зависеть от конкретного класса и знать обо всех его полях. Копирование снаружи ломает инкапсуляцию, не видит закрытое состояние и быстро устаревает при изменении класса.

Решение

Копирование поручается самому объекту-прототипу. Общий метод Clone() создаёт экземпляр того же фактического типа и переносит в него всё необходимое состояние. Клиент работает через интерфейс прототипа и не знает, какой конкретный класс клонируется.

Структура

  • Prototype - общий контракт клонирования.
  • ConcretePrototype - знает, как скопировать собственные, в том числе закрытые, поля.
  • Client - запрашивает копию через общий контракт.
  • PrototypeRegistry (опционально) - хранит заранее настроенные эталонные экземпляры по ключу и выдаёт их копии.

Варианты реализации

  • Конструктор копирования - типобезопасный и явный вариант: new Document(source). Каждый уровень иерархии копирует объявленные им поля.
  • Полиморфный Clone() - позволяет клонировать коллекцию объектов через общий интерфейс, не проверяя их конкретные типы.
  • Реестр прототипов - полезен, когда есть набор популярных конфигураций: вместо множества подклассов клиент просит копию именованного шаблона.
  • Поверхностная или глубокая копия - поверхностная копирует ссылки, глубокая создаёт новые изменяемые вложенные объекты. Это не два разных паттерна, а важный выбор семантики клонирования.

В .NET не следует бездумно использовать MemberwiseClone() или устаревший ICloneable: первый делает только поверхностную копию, а второй не сообщает вызывающему коду, какую именно копию он получит. Обычно понятнее собственный типизированный контракт, конструктор копирования или record-выражение with для моделей со значимой семантикой значения.

Когда применять

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

Плюсы

  • Клиент не зависит от конкретных классов клонируемых объектов.
  • Повторно используется сложная конфигурация без дублирования кода инициализации.
  • Реестр прототипов позволяет добавлять новые шаблоны во время выполнения.

Минусы

  • Глубокое копирование графов с циклами и разделяемыми ссылками сложно реализовать корректно.
  • Неочевидная семантика поверхностной копии приводит к случайному общему изменяемому состоянию.
  • При добавлении нового поля нужно не забыть включить его в копирование.

Отличие от Builder

Builder создаёт объект по шагам из переданных параметров. Prototype начинает с уже готового состояния и получает новый объект копированием. Их можно сочетать: Builder подготавливает эталон, а Prototype быстро размножает его.

Пример в .NET / C

  • record и выражение with создают неглубокую копию с изменением выбранных компонентов.
  • Array.Clone() создаёт поверхностную копию массива.
  • MemberwiseClone() доступен наследникам object, но требует явной обработки вложенного изменяемого состояния.

Источник для сверки структуры: Refactoring.Guru - Prototype.

Пример реализации на C#

Prototype.cs
using System;
using System.Collections.Generic;

namespace DesignPatterns.Creational.Prototype
{
    public interface IPrototype<out T>
    {
        T Clone();
    }

    // Конкретный прототип сам копирует своё состояние, включая закрытые поля.
    public sealed class Document : IPrototype<Document>
    {
        private readonly List<string> _pages;

        public string Title { get; }
        public IReadOnlyList<string> Pages => _pages.AsReadOnly();

        public Document(string title, IEnumerable<string> pages)
        {
            Title = title;
            _pages = new List<string>(pages);
        }

        private Document(Document source)
        {
            Title = source.Title;
            // Глубокая копия изменяемой коллекции: клон не делит список с оригиналом.
            _pages = new List<string>(source._pages);
        }

        public Document Clone() => new(this);

        public void AddPage(string text) => _pages.Add(text);
    }

    // Необязательное хранилище заранее настроенных прототипов.
    public sealed class DocumentRegistry
    {
        private readonly Dictionary<string, Document> _prototypes = new();

        public void Register(string key, Document prototype) =>
            _prototypes[key] = prototype;

        public Document Create(string key) =>
            _prototypes.TryGetValue(key, out Document? prototype)
                ? prototype.Clone()
                : throw new KeyNotFoundException($"Прототип '{key}' не зарегистрирован.");
    }

    public static class Demo
    {
        public static void Run()
        {
            var registry = new DocumentRegistry();
            registry.Register("invoice", new Document(
                "Счёт",
                new[] { "Реквизиты", "Позиции", "Итого" }));

            Document first = registry.Create("invoice");
            Document second = registry.Create("invoice");
            first.AddPage("Комментарий для первого клиента");

            Console.WriteLine(first.Pages.Count);  // 4
            Console.WriteLine(second.Pages.Count); // 3: коллекция скопирована глубоко
        }
    }
}

Открыть Prototype.cs отдельно Скачать Prototype.cs