В этой статье будет рассмотрен паттерн проектирования Прототип C# — Prototype C#, для чего он нужен и какие проблемы он решает, где можно применять данный шаблон и когда это будет излишним.
Идея паттерна Прототип (Prototype)
Паттерн проектирования — это продуманный способ построения исходного кода программы для решения часто возникающих в повседневном программировании проблем проектирования. Иными словами, это уже придуманное решения, для типичной задачи. При этом паттерн не готовое решение, а просто алгоритм действий, который должен привести к желаемому результату. Давайте рассмотрим один из наиболее часто используемых порождающих паттернов — Прототип (Prototype).
Как я уже писал ранее, существует три вида паттернов проектирования:
- Порождающие паттерны позволяют возможность выполнять инициализацию объектов наиболее удобным и оптимальным способом.
- Структурные паттерны описывают взаимоотношения между различными классами или объектами, позволяя им совместно реализовывать поставленную задачу.
- Поведенческие паттерны позволяют грамотно организовать связь между сущностями для оптимизации и упрощения их взаимодействия.
Прототип (Prototype) — это порождающий паттерн, который позволяет создавать новый экземпляр класса на основе уже имеющегося объекта. При этом все значения нового объекта должны быть идентичны изначальному. То есть, данный паттерн определяет метод клонирования объекта.
Подпишись на группу Вконтакте и Телеграм-канал. Там еще больше полезного контента для программистов.
А на YouTube-канале ты найдешь обучающие видео по программированию. Подписывайся!
Архитектура шаблона Prototype

- Client — использует экземпляры классов и создает новые при помощи клонирования
- Prototype — базовый абстрактный класс или интерфейс определяющий прототип и метод клонирования
- ConcretePrototype — конкретные наследники базового класса или интерфейса прототипирования, реализующие его. Один из экземпляров создан на основе второго.
Логика работы Прототипа
Рассмотрим основную логику работы паттерна. Здесь нам нужно немного отойти в сторону. Нужно понимать, что в языке C# существуют переменные двух типов:
- Переменные значения — это все переменные стандартных типов данных (int, bool, double и т.д.), которые имеют фиксированный размер и хранятся в стеке.
- Ссылочные значения — это переменные, данные которых хранятся в куче, а в стеке хранится только указатель на область памяти. В эту категорию попадают string и все другие системные или создаваемые тобой классы.
Так вот, при копировании значимых переменных никаких проблем не возникает, в стеке создается еще одно значение. Но если мы копируем ссылочную переменную, мы копируем указатель на область памяти, и соответственно при изменении значения в одном из клонов, значения изменятся у всех объектов. В тех случаях, когда нам нужно избежать данной проблемы при создании новых экземпляров класса на основе уже имеющихся и нужно применять данный паттерн. Основная идея паттерна Прототип заключается в том, что необходимо предоставить интерфейс клонирования объекта в сам объект.
На самом деле, реализация данного паттерна очень часто используется на практике, поэтому в пакет .NET уже включен интерфейс для клонирования объектов. Чтобы не изобретать велосипед мы будем пользоваться им.
Реализация паттерна проектирования Прототип (Prototype) на языке C#
Для начала создадим обычный класс для сравнения. Он будет хранить в себе обычные текстовые данные.
using System; namespace Prototype { /// <summary> /// Обычный класс с текстом. /// </summary> public class Text { /// <summary> /// Количество символов на странице. /// </summary> private readonly int _symbolsOnPage = 100; /// <summary> /// Хранимые текстовые данные. /// </summary> public string Content { get; set; } /// <summary> /// Количество страниц текста. /// </summary> public int Pages { get { if(Content != null) { return Content.Length / _symbolsOnPage + 1; } else { return 0; } } } /// <summary> /// Создать новый экземпляр обычного текста. /// </summary> /// <param name="content"> Текстовые данные. </param> public Text(string content) { // Проверяем выходные данные на корректность. if(string.IsNullOrEmpty(content)) { throw new ArgumentNullException(nameof(content)); } // Устанавливаем значение. Content = content; } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns> Хранимые текстовые данные. </returns> public override string ToString() { return Content; } } }
Теперь создадим класс реализующий интерфейс ICloneable. Он будет практически идентичен первому, за исключением необходимости реализации метода Clone()
using System; namespace Prototype { /// <summary> /// Клонируемый объект с текстом. /// </summary> public class TextCloneable : ICloneable { /// <summary> /// Количество символов на странице. /// </summary> private readonly int _symbolsOnPage = 100; /// <summary> /// Хранимые текстовые данные. /// </summary> public string Content { get; set; } /// <summary> /// Количество страниц текста. /// </summary> public int Pages { get { if (Content != null) { return Content.Length / _symbolsOnPage + 1; } else { return 0; } } } /// <summary> /// Создать новый экземпляр клонируемого текста. /// </summary> /// <param name="content"> Текстовые данные. </param> public TextCloneable(string content) { // Проверяем входные данные на корректность. if (string.IsNullOrEmpty(content)) { throw new ArgumentNullException(nameof(content)); } // Устанавливаем значения. Content = content; } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns> Хранимые текстовые данные. </returns> public override string ToString() { return Content; } /// <summary> /// Клонировать объект. /// </summary> /// <returns> Дубликат объекта. </returns> public object Clone() { // Поверхностное копирование. // return MemberwiseClone(); // Глубокое копирование. var newContent = Content.Clone().ToString(); var newText = new TextCloneable(newContent); return newText; } } }
Обратите внимание, что выполнять клонирование объекта возможно двумя способами:
- Поверхностное копирование — реализуется с помощью стандартного метода MemberwiseClone(). Его недостаток заключается в том, клонирование будет выполнено только для переменных хранимых по значению. Если класс содержит поля или свойства ссылочного типа, они не будут клонированы.
- Глубокое копирование — реализуется вручную. Необходимо самому реализовать создание нового объекта со всеми полями и свойствами класса.
Теперь посмотрим как будут вести себя объекты Text и TextCloneable при копировании.
using System; namespace Prototype { class Program { static void Main(string[] args) { // Проверяем как ведут себя значимые типы данных. // Создаем два объекта, во второй помещаем первый, // а затем изменяем второй. Первый остается неизменным. var int1 = 5; var int2 = int1; int2 = 7; Console.WriteLine(int1); Console.ReadLine(); // Проверяем как ведут себя обычные ссылочные типы данных. // Создаем два объекта, во второй помещаем первый, // а затем изменяем второй. Первый объект тоже изменился. var text1 = new Text("Hello, World!"); var text2 = text1; text2.Content = "Good bay, World!"; Console.WriteLine(text1.Content); Console.ReadLine(); // Проверяем как ведут себя клонируемые объекты. // Создаем два объекта, во второй помещаем клон первого, // а затем изменяем второй. Первый остается неизменным. var cloneText1 = new TextCloneable("Hello, World!"); var cloneText2 = cloneText1.Clone() as TextCloneable ?? throw new NullReferenceException("Не удалось выполнить приведение типа"); cloneText2.Content = "Good bay, World!"; Console.WriteLine(cloneText1.Content); Console.ReadLine(); } } }
Удостовериться в правильности можно взглянув на консоль после выполнения программы

Как мы можем увидеть, в первом случае объект изначальный объект был изменен, а во втором — нет.
Исходный код приложения доступен в репозитории github.
Также рекомендую изучить статью Паттерн Фабричный метод (Factory Method). А также подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.