Порождающие шаблоны: Прототип (Prototype)

Название шаблона

Прототип (Prototype).

Тип

Порождающий шаблон проектирования (Creational).

Описание

PrototypeШаблон Прототип позволяет создавать новые объекты путем клонирования уже существующих.

Данный шаблон используется в случаях, если:

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

Прототип, как и многие другие порождающие шаблоны, скрывает реализацию создаваемого объекта.

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

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

Схожие шаблоны и их отличия

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

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

  • разрабатываем интерфейс или базовый класс, содержащий метод клонирования 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();
    }
}

Комментарии (10) -

Если позволите, несколько дополнений:
1. Динамический Фабричный метод можно объявить generic и наложить ограничение на тип в виде IDeepCloneable, тогда не нужно будет проверять это внутри;
2. IShallowCloneable и IDeepCloneable можно заменить одним интерфейсом с методом, принимающим переменную типа bool для выбора типа копирования;
3. binFormatter лучше создавать непосредственно по месту использования (в using блоке);
4. Не лишним было бы упомянуть о вопросах производительности и случаях кольцевых ссылок.

@ osmirnov: Дополнениям только рад Smile

1. Можно, т.е. получится так

public static void AddPrototype<T>(T prototype) where T : IDeepCloneable

Но если придется передавать объект как, например, object, то получим ошибку. Т.е. то, что передаваемый объект поддерживает IDeepCloneable, должно быть известно на этапе компиляции. Вариант с runtime проверкой более универсальный.

2. Была такая мысль, но все же это неудачный дизайн. Причем уходим не далеко от ICloneable.
- не всегда нужны оба типа клонирования
- по сути у нас будет 3 метода: public для вызова (который всегда будет одинаковый и по сути не нужный), и 2 private для каждого типа копирования.
- как проверить поддерживается ли какое-то конкретное копирование? Только ловить exception. В случае с 2 интерфейсами мы сделаем это проще, и даем клиентскому коду самому определять дальнейшие действия.

Вариант с одним интерфейсом и 2 методами так же не подходит по последней причине.

3. Согласен.

4. Про производительность немного упомянуто (мне кажется этого достаточно чтобы понять суть вопроса), а вот про кольцевые ссылки добавлю. Но так же вскользь, а то добавление может получиться не меньше самой статьи.

не забываем automapper

Cогласен.

Очень хорошо и понятно изложено. Спасибо

Пожалуйста.

Денис 14.11.2011 12:48:37

Мне понравилось!

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

@ E1ektr0: А пример с IDeepCloneable чем не подходит? Если мешают [Serializable], то можно создавать объекты вручную через Reflection. Тогда можно, например, на время копирования завести словарь, где содержать соответствие старых объектов и их клонов. И перед созданием нового объекта сверяться по нему.

Спасибо, ответ исчерпывающий Smile

Добавить комментарий