prototypeПаттерн Прототип (Prototype) позволяет создавать объекты на основе уже ранее созданных объектов-прототипов. То есть по сути данный паттерн предлагает технику клонирования объектов.

Когда использовать Прототип?

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

 

Формальная структура паттерна на в C# могла бы выглядеть следующим образом:

class Client
{
    void Operation()
    {
        Prototype prototype = new ConcretePrototype1(1);
        Prototype clone = prototype.Clone();
        prototype = new ConcretePrototype2(2);
        clone = prototype.Clone();
    }
}
 
abstract class Prototype
{
    public int Id { get; private set; }
    public Prototype(int id)
    {
        this.Id = id;
    }
    public abstract Prototype Clone();
}
 
class ConcretePrototype1 : Prototype
{
    public ConcretePrototype1(int id)
        : base(id)
    { }
    public override Prototype Clone()
    {
        return new ConcretePrototype1(Id);
    }
}
 
class ConcretePrototype2 : Prototype
{
    public ConcretePrototype2(int id)
        : base(id)
    { }
    public override Prototype Clone()
    {
        return new ConcretePrototype2(Id);
    }
}

Участники

  • Prototype: определяет интерфейс для клонирования самого себя, который, как правило, представляет метод Clone()
  • ConcretePrototype1 и ConcretePrototype2: конкретные реализации прототипа. Реализуют метод Clone()
  • Client: создает объекты прототипов с помощью метода Clone()

Рассмотрим клонирование на примере фигур – прямоугольников и кругов:

class Program
{
    static void Main(string[] args)
    {
        IFigure figure = new Rectangle(30,40);
        IFigure clonedFigure = figure.Clone();
        figure.GetInfo();
        clonedFigure.GetInfo();
 
        figure = new Circle(30);
        clonedFigure=figure.Clone();
        figure.GetInfo();
        clonedFigure.GetInfo();
 
        Console.Read();
    }
}
 
interface IFigure
{
    IFigure Clone();
    void GetInfo();
}
 
class Rectangle: IFigure
{
    int width;
    int height;
    public Rectangle(int w, int h)
    {
        width = w;
        height = h;
    }
 
    public IFigure Clone()
    {
        return new Rectangle(this.width, this.height);
    }
    public void GetInfo()
    {
        Console.WriteLine("Прямоугольник длиной {0} и шириной {1}", height, width);
    }
}
 
class Circle : IFigure
{
    int radius;
    public Circle(int r)
    {
        radius = r;
    }
 
    public IFigure Clone()
    {
        return new Circle(this.radius);
    }
    public void GetInfo()
    {
        Console.WriteLine("Круг радиусом {0}", radius);
    }
}

Здесь в качестве прототипа используется интерфейс IFigure, который реализуется классами Circle и Rectangle.

Но в данном случае надо заметить, что фреймворк .NET предлагает функционал для копирования в виде метода MemberwiseClone(). Например, мы могли бы изменить реализацию метода Clone() в классах прямоугольника и круга следующим образом:

public IFigure Clone()
{
    return this.MemberwiseClone() as IFigure;
}

Причем данный метод был бы общим для обоих классов. И работа программы никак бы не изменилась.

В то же время надо учитывать, что метод MemberwiseClone() осуществляет неполное копирование – то есть копирование значимых типов. Если же класс фигуры содержал бы объекты ссылочных типов, то оба объекта после клонирования содержали бы ссылку на один и тот же ссылочный объект. Например, пусть фигура круг имеет свойство ссылочного типа:

class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}
class Circle : IFigure
{
    int radius;
    public Point Point { get; set; }
    public Circle(int r, int x, int y)
    {
        radius = r;
        this.Point = new Point { X = x, Y = y };
    }
 
    public IFigure Clone()
    {
        return this.MemberwiseClone() as IFigure;
    }
    public void GetInfo()
    {
        Console.WriteLine("Круг радиусом {0} и центром в точке ({1}, {2})", radius, Point.X, Point.Y);
    }
}

В этом случае при изменении значений в свойстве Point начальной фигуры автоматически бы изменилось соответствующее значение и у клонированной фигуры:

Circle figure = new Circle(30, 50, 60);
Circle clonedFigure=figure.Clone() as Circle;
figure.Point.X = 100; // изменяем координаты начальной фигуры
figure.GetInfo(); // figure.Point.X = 100
clonedFigure.GetInfo(); // clonedFigure.Point.X = 100

Чтобы избежать подобной ситуации, надо применить полное копирование:

using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
 
//........................
class Program
{
    static void Main(string[] args)
    {
        Circle figure = new Circle(30, 50, 60);
        // применяем глубокое копирование
        Circle clonedFigure=figure.DeepCopy() as Circle;
        figure.Point.X = 100;
        figure.GetInfo();
        clonedFigure.GetInfo();
 
        Console.Read();
    }
}
//.........................
     
[Serializable]
class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}
[Serializable]
class Circle : IFigure
{
    int radius;
    public Point Point { get; set; }
    public Circle(int r, int x, int y)
    {
        radius = r;
        this.Point = new Point { X = x, Y = y };
    }
 
    public IFigure Clone()
    {
        return this.MemberwiseClone() as IFigure;
    }
 
    public object DeepCopy()
    {
        object figure = null;
        using (MemoryStream tempStream = new MemoryStream())
        {
            BinaryFormatter binFormatter = new BinaryFormatter(null,
                new StreamingContext(StreamingContextStates.Clone));
 
            binFormatter.Serialize(tempStream, this);
            tempStream.Seek(0, SeekOrigin.Begin);
 
            figure = binFormatter.Deserialize(tempStream);
        }
        return figure;
    }
    public void GetInfo()
    {
        Console.WriteLine("Круг радиусом {0} и центром в точке ({1}, {2})", radius, Point.X, Point.Y);
    }
}

Чтобы вручную не создавать у клонированного объекта вложенный объект Point, здесь используются механизмы бинарной сериализации. И в этом случае все классы, объекты которых подлежат копированию, должны быть помечены атрибутом Serializable.

Реализация шаблона в общем виде

  • разрабатываем интерфейс или базовый класс, содержащий метод клонирования Clone();
  • создаем реализации или потомков, включая их варианты метода клонирования;
  • при необходимости, создаем фабрику, которая используется для управления прототипами;
  • в клиентском коде, для создания нового объекта, обращаемся к объекту или фабрике.

Примеры реализации

1. Клонирование, как метод класса или интерфейса

Рассмотрим часть приложения для рисования блок-схем. Класс SchemeElement задает элемент схемы:

public abstract class SchemeElement
{
    public uint Id { get; set; }
    public string Title { get; set; }
     
    public virtual SchemeElement Clone()
    {
        return (SchemeElement)this.MemberwiseClone();
    }
}

Операция клонирования реализована в методе Clone(). Он объявлен как virtual, поскольку потомкам класса может потребоваться своя реализация. Вызываемый метод MemberwiseClone() определен в классе Object и обеспечивает неполное копирование для полей объекта. Этот метод и разновидности операции копирования будут рассмотрены чуть позже.

Создадим реализации SchemeElement для разных элементов (подробности убраны для краткости):

public class BoxElement : SchemeElement { }
public class CircleElement : SchemeElement { }
public class ConnectorElement : SchemeElement { }

Теперь представим, что необходимо разработать метод, который добавляет на схему копии выбранных элементов. Их список передается в виде объектов базового типа SchemeElement. Использовать операцию new для создания новых объектов невозможно, т.к. неизвестен их конкретный тип. Применение шаблона Прототип легко решает эту проблему:

public void InsertCopy(IEnumerable<SchemeElement> selectedElements)
{
    foreach (SchemeElement element in selectedElements) {
        SchemeElement newElement = (SchemeElement)element.Clone();
 
        // The Id must be unique
        newElement.Id = this.GetNewId();
 
        // TODO: Setup the new element
 
        // Add the element to the scheme
        this.AddNewElement(newElement);
    }
}

Одновременно решена и задача копирования полей и свойств. Обратите внимание на корректировку значения Id. Иногда, после клонирования, требуется переопределить часть свойств объекта.

2. Использование фабрики прототипов

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

Рассмотрим вариант совместного использования с Абстрактной фабрикой. Создадим статический класс ElementFactory. В конструкторе породим эталонные прототипы и настроим их параметры. В дальнейшем, создание новых объектов будет осуществляться их клонированием. Код достаточно простой:

public static class ElementFactory
{
    private static readonly BoxElement _boxPropotype;
    private static readonly CircleElement _circlePropotype;
    private static readonly ConnectorElement _connectorPropotype;
 
    public static ElementFactory()
    {
        // Create and setup prototypes
        _boxPropotype = new BoxElement();
        _boxPropotype.Title = "New box element";
 
        _circlePropotype = new CircleElement();
        _circlePropotype.Title = "New circle element";
 
        _connectorPropotype = new ConnectorElement();
        _connectorPropotype.Title = "New connector";
    }
 
    public static BoxElement CreateBox()
    {
        return (BoxElement)_boxPropotype.Clone();
    }
 
    public static CircleElement CreateCircle()
    {
        return (CircleElement)_circlePropotype.Clone();
    }
 
    public static ConnectorElement CreateConnector()
    {
        return (ConnectorElement)_connectorPropotype.Clone();
    }
}

Использование:

BoxElement newBox = ElementFactory.CreateBox();

3. Динамический Фабричный метод

Перейдем к обещанному варианту совместного использования Прототипа и параметризованного Фабричного метода. Для его реализации создадим список прототипов. В него будем добавлять только объекты, поддерживающие интерфейс IDeepCloneable.

public static class DynamicFabric
{
    private static Dictionary<Type, object> _prototypes = new Dictionary<Type, object>();
 
    public static void AddPrototype(object prototype)
    {
        IDeepCloneable iClone = prototype as IDeepCloneable;
        if (iClone == null) {
            throw new ArgumentException("prototype must implement ICloneable");
        }
 
        _prototypes.Add(prototype.GetType(), prototype);
    }
 
    /// Factory method
    public static object CreateObject(Type type)
    {
        object prototype;
        _prototypes.TryGetValue(type, out prototype);
         
        IDeepCloneable iClone = prototype as IDeepCloneable;
        if (iClone == null) {
            throw new ArgumentException("unknown type: " + type.ToString());
            // or return null;
        }
 
        return iClone.DeepClone();
    }
}

Для использования такого Фабричного метода необходимо сначала передать ему эталонные прототипы. После этого, при необходимости, вызывать метод CreateObject(), указав тип создаваемого объекта.

BoxElement boxPrototype = new BoxElement();
DynamicFabric.AddPrototype(boxPrototype);
 
CircleElement circlePrototype = new CircleElement();
DynamicFabric.AddPrototype(circlePrototype);
// ...
BoxElement newBox = (BoxElement)DynamicFabric.CreateObject(typeof(BoxElement));

4. Использование библиотек для отображения (mapping)

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

Использование отображения (mapping) – обширная тема, которая легко займет не одну страницу. Поэтому ограничимся ссылками на пару библиотек: AutoMapper и Emit Mapper. Начать их изучение порекомендую с достаточно простого для понимания раздела “Getting started” библиотеки Emit Mapper.

Особенности реализации в .NET и C#

В .NET существует два типа переменных: значимые и ссылочные. Для значимых типов при копировании всегда создается копия. Поэтому про них больше упоминать не будем. А вот переменные ссылочного типа при копировании имеют особенность. Она заключается в том, что передается ссылка на существующий экземпляр. При этом копия объекта не создается. Поэтому различают два варианта клонирования:

  1. Неполное копирование. В результате, ссылочные переменные копии указывают на те же объекты, что и в прототипе. Этот вариант реализован в protected методе MemberwiseClone() класса Object. Такое клонирование объекта происходит, как правило, быстро, но стоит учитывать два момента:
    • получаемая копия взаимосвязана с прототипом;
    • существуют объекты, например string, которые при копировании возвращают новый экземпляр.
  2. Полное копирование. При этом ссылочные переменные копии получат ссылки на собственные объекты, которые так же были полностью скопированы. Такое клонирование медленнее и сложнее. Особенно это заметно, если вложенные объекты самостоятельно не поддерживают свое полное копирование. Кроме того, необходимо определять и особо учитывать кольцевые ссылки. Т.е. ситуации, когда два или более вложенных объектов ссылаются друг на друга. Но, несмотря на эти возможные сложности, данный вариант позволяет создать копию, которая полностью независима от прототипа.

Необходимо отметить, что в .NET есть стандартный интерфейс для клонирования объектов – ICloneable. Он включает в себя единственный метод Clone().

Может возникнуть вопрос что лучше: реализовывать ICloneable или свой интерфейс? К сожалению, в дизайне интерфейса ICloneable есть серьезные упущения – нет явного указания какой вариант копирования он поддерживает и нет возможности реализовать оба варианта копирования. Поэтому, на мой взгляд:

  • реализовывать ICloneable необходимо явно (explicitly) и только когда этого требует взаимодействие с другими объектами;
  • ICloneable.Clone() должен осуществлять полное копирование;
  • в остальных случаях стоит создать и использовать два интерфейса: IShallowCloneable для неполного и IDeepCloneable для полного копирования.

Таким образом, для реализации неполного копирования на C# достаточно вызвать MemberwiseClone().

Этот же вариант подойдет для полного копирования, если все поля – значимые типы или классы, возвращающие новый экземпляр при копировании. В остальных случаях данную операцию необходимо реализовывать самостоятельно, с учетом особенностей конкретного класса. Например, копируя поля по одному (если их мало), используя Reflection или сериализацию.

С учетом сказанного, схема реализации шаблона Прототип на C# будет выглядеть следующим образом:

public interface IShallowCloneable
{
    object ShallowClone();
}
 
public interface IDeepCloneable
{
    object DeepClone();
}
 
// Serializable attribute is required for the DeepClone() method
[Serializable]
public class MyClassName : IShallowCloneable, IDeepCloneable, ICloneable
{
    // TODO: Add properties
    // TODO: Add fields
    // TODO: Add methods
 
    public object ShallowClone()
    {
        return this.MemberwiseClone();
    }
 
    public object DeepClone()
    {
        object clone = null;
 
        // Make deep copy using serialization
        using (MemoryStream tempStream = new MemoryStream())
        {
            BinaryFormatter binFormatter = new BinaryFormatter(null,
                new StreamingContext(StreamingContextStates.Clone)); 
 
            binFormatter.Serialize(tempStream, this);
            tempStream.Seek(0, SeekOrigin.Begin);
 
            clone = binFormatter.Deserialize(tempStream);
        }
 
        return clone;
    }
 
    object ICloneable.Clone()
    {
        return this.DeepClone();
    }
}