Prototype Pattern

Definition

This design pattern is all about object copying, the use case would be when its easier to copy (clone) an existing object than to fully initialize a new one. Note that the existing object could be partially or fully constructed.

These would be complicated objects, perhaps constructed with a builder and you now wish to make simple changes to it for your use case.

A Deep Copy is what is required for the Prototype Pattern.

Copy Description
Shallow A shallow copy is a copy of the object and its references. This means after you make the copy changes to this object also makes changes to the other object.
Deep A deep copy is not just a copy of the object but a copy of all its references (on the heap) by making new objects which replicates the state of those references. This would need to be done recursively so you would need to make a complete copy of the object. This means the change in this object does not affect the other object.

Caveats

  • When deep copying an entity, if it has a database auto assigned ID you may want to reset this as should the copied entity change and then be persisted you may update persisted data by mistake.

ICloneable

Don’t use ICloneable for the Prototype Pattern as it will create a shallow copy.

Copy Constructors

Don’t use Copy Constructors as this would not be idiomatic for a C# developer and requires each custom type to have a copy constructor to allow traversing of the tree.

Explicit Deep Copy Interface

You could create your own Explicit Deep Copy Interface like IProtoType<T> however this would require manual object traversing and having each dependent object in the tree implement IProtoType<T>.

Copy Through Serialization (Binary)

Serialization will be slower but will traverse the entire object tree. As we will not be persisting the serialized object to storage it can simply be kept in a MemoryStream.

Using a BinaryFormatter makes the most sense as we don’t need human readable data (XML, JSON).

Create the extension method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static class ExtensionMethods
{
public static T DeepCopy<T>(this T self)
{
using (var stream = new MemoryStream())
{
var formatter = new BinaryFormatter();
formatter.Serialize(stream, self);
stream.Seek(0, SeekOrigin.Begin); // Rewind the stream with offset of 0 from the begining

object copy = formatter.Deserialize(stream);
stream.Close();
return (T)copy;
}
}
}

Create some classes to hold the state to be copied, caveat is the classes need to have the [Serializable] attribute / annotation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[Serializable]
public class Person
{
public string FirstName;
public Address Address;

public Person(string firstName, Address address)
{
FirstName = firstName;
Address = address;
}

public override string ToString()
{
return $"{nameof(FirstName)}: {FirstName}, {nameof(Address)}: {Address}";
}
}

[Serializable]
public class Address
{
public string StreetName;
public int HouseNumber;

public Address(string streetName, int houseNumber)
{
StreetName = streetName;
HouseNumber = houseNumber;
}

public override string ToString()
{
return $"{nameof(StreetName)}: {StreetName}, {nameof(HouseNumber)}: {HouseNumber}";
}
}

Instantiate and copy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var carl = new Person("Carl", new Address("Sale Street", 66));

Console.WriteLine("Initial Object:");
Console.WriteLine(carl);
Console.WriteLine("-----------------------");

var john = carl.DeepCopy();
john.FirstName = "John";
john.Address.HouseNumber = 789;
john.Address.StreetName = "Foo Street";

Console.WriteLine("Initial Object & Copy:");
Console.WriteLine(carl);
Console.WriteLine(john);

Output

1
2
FirstName: Carl, Address: StreetName: Sale Street, HouseNumber: 66
FirstName: John, Address: StreetName: Foo Street, HouseNumber: 789

Copy Through Serialization (XML)

The classes you wish to serialize may not be open for modification, so you could use XMlSerializer if you cannot add the annotation. Caveat would however be that the classes need a parameterless constructor.

The extension method would then look like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static class ExtensionMethods
{
public static T DeepCopy<T>(this T self)
{
using (var stream = new MemoryStream())
{
var xmlSerializer = new XmlSerializer(typeof(T));
xmlSerializer.Serialize(stream, self);
stream.Position = 0; // same as `stream.Seek(0, SeekOrigin.Begin);`

return (T) xmlSerializer.Deserialize(stream);
}
}
}

Copy Through Serialization (System.Text.Json)

Some Caveats still exist:

  • Parameterless constructor is needed
  • This serializer doesn’t work with fields but needs properties instead.
1
2
3
4
5
6
7
8
9
public class Person
{
public string FirstName { get; set; }
...

public class Person
{
public string FirstName;
...

The extension method would look like this:

1
2
3
4
5
6
7
8
9
10
11
public static class ExtensionMethods
{
public static T DeepCopy<T>(this T self)
{
using (var stream = new MemoryStream())
{
var json = JsonSerializer.Serialize(self);
return JsonSerializer.Deserialize<T>(json);
}
}
}

Copy Through Serialization (Newtonsoft.Json)

No additional changes are needed to the class that I could see. The extension method would look like this:

1
2
3
4
5
6
7
8
public static class ExtensionMethods
{
public static T DeepCopy<T>(this T self)
{
var json = JsonConvert.SerializeObject(self);
return JsonConvert.DeserializeObject<T>(json);
}
}

References