Creating Copies Without Reconstruction
If you've ever needed to create many objects with similar configurations, you know how tedious it is to call constructors repeatedly with the same parameters or rebuild complex object graphs from scratch. Prototypes let you clone existing objects instead of reconstructing them.
The Prototype pattern creates new instances by copying existing ones. You start with a prototype object that's already configured, then clone it when you need duplicates. This is faster than instantiating and initializing from scratch, especially for objects with expensive setup.
You'll build a document template system that clones pre-configured documents. By the end, you'll understand shallow versus deep copying and when each makes sense.
Shallow Copying with MemberwiseClone
C# provides MemberwiseClone for shallow copying. It duplicates value type fields and copies references for reference types. The clone shares references to the same nested objects as the original.
namespace PrototypeDemo;
public class Address
{
public string Street { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
}
public class Person
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
public Address Address { get; set; } = new();
public Person ShallowCopy()
{
return (Person)MemberwiseClone();
}
}
// Usage
var original = new Person
{
Name = "Alice",
Age = 30,
Address = new Address { Street = "123 Main", City = "NYC" }
};
var clone = original.ShallowCopy();
clone.Name = "Bob";
clone.Address.City = "LA";
Console.WriteLine($"Original: {original.Name}, {original.Address.City}");
Console.WriteLine($"Clone: {clone.Name}, {clone.Address.City}");
The clone's Name change doesn't affect the original because string is immutable and gets a new reference. But modifying the Address affects both because they share the same Address instance. This is shallow copying's key characteristic.
Deep Copying for Independent Objects
Deep copy recursively clones all nested objects. The clone becomes fully independent with no shared references. Changes to nested objects in the clone don't affect the original.
namespace PrototypeDemo;
public class AddressDeep
{
public string Street { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public AddressDeep Clone() =>
new() { Street = Street, City = City };
}
public class PersonDeep
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
public AddressDeep Address { get; set; } = new();
public PersonDeep DeepClone()
{
return new PersonDeep
{
Name = Name,
Age = Age,
Address = Address.Clone()
};
}
}
// Usage
var original = new PersonDeep
{
Name = "Alice",
Age = 30,
Address = new AddressDeep { Street = "123 Main", City = "NYC" }
};
var clone = original.DeepClone();
clone.Address.City = "LA";
Console.WriteLine($"Original city: {original.Address.City}");
Console.WriteLine($"Clone city: {clone.Address.City}");
Now modifying the clone's address doesn't touch the original because Address.Clone creates a new instance. Each person has a completely independent address object.
Prototype Registry for Template Management
A registry stores prototype objects that clients can clone. Instead of knowing how to construct objects, clients ask the registry for a prototype by name and clone it.
namespace PrototypeDemo;
public interface IPrototype<T>
{
T Clone();
}
public class Document : IPrototype<Document>
{
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public List<string> Tags { get; set; } = new();
public Document Clone()
{
return new Document
{
Title = Title,
Content = Content,
Tags = new List<string>(Tags)
};
}
}
public class DocumentRegistry
{
private readonly Dictionary<string, Document> _prototypes = new();
public void RegisterPrototype(string key, Document prototype)
{
_prototypes[key] = prototype;
}
public Document CreateDocument(string key)
{
if (!_prototypes.ContainsKey(key))
throw new ArgumentException($"Prototype {key} not found");
return _prototypes[key].Clone();
}
}
Clients register prototypes once, then create instances by cloning. This centralizes template management and hides construction complexity behind simple clone calls.
Using the Prototype Registry
Set up prototypes during initialization, then clone them throughout your application. Each clone starts with the same baseline configuration but can be modified independently.
using PrototypeDemo;
var registry = new DocumentRegistry();
registry.RegisterPrototype("report", new Document
{
Title = "Monthly Report",
Content = "Report template",
Tags = new List<string> { "business", "monthly" }
});
registry.RegisterPrototype("letter", new Document
{
Title = "Business Letter",
Content = "Dear Sir/Madam,",
Tags = new List<string> { "correspondence" }
});
var report1 = registry.CreateDocument("report");
report1.Content = "January sales data";
var report2 = registry.CreateDocument("report");
report2.Content = "February sales data";
Console.WriteLine($"Report 1: {report1.Content}");
Console.WriteLine($"Report 2: {report2.Content}");
Both reports start from the same prototype but have different content. Cloning saves you from repeating the title and tags setup for every report.
Try It Yourself
Build a simple prototype system that clones game characters with default equipment. This shows how prototypes speed up creation of preconfigured objects.
Steps
- Scaffold:
dotnet new console -n PrototypeDemo
- Enter:
cd PrototypeDemo
- Modify Program.cs
- Update .csproj
- Execute:
dotnet run
var warrior = new Character("Warrior", 100, new List<string> { "Sword", "Shield" });
var mage = new Character("Mage", 60, new List<string> { "Staff", "Spellbook" });
var player1 = warrior.Clone();
player1.Name = "Conan";
var player2 = mage.Clone();
player2.Name = "Gandalf";
Console.WriteLine($"{player1.Name}: HP={player1.Health}, Items={string.Join(",", player1.Items)}");
Console.WriteLine($"{player2.Name}: HP={player2.Health}, Items={string.Join(",", player2.Items)}");
class Character
{
public string Name { get; set; }
public int Health { get; set; }
public List<string> Items { get; set; }
public Character(string name, int health, List<string> items)
{
Name = name;
Health = health;
Items = items;
}
public Character Clone() =>
new(Name, Health, new List<string>(Items));
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Run result
Conan: HP=100, Items=Sword,Shield
Gandalf: HP=60, Items=Staff,Spellbook
Knowing the Limits
Skip prototypes when object creation is cheap and straightforward. If a constructor call with a few parameters is fast, cloning adds unnecessary complexity. Use prototypes when initialization is expensive or involves complex configuration that you want to reuse.
Avoid prototypes for objects with circular references or deep nesting that's hard to clone correctly. Deep copying complex graphs can be error-prone and slow. If serialization is your only option for deep cloning, the pattern might not be worth the overhead.
Watch for shared mutable state. If you forget to deep copy a nested collection or object, clones share references and modifications propagate unexpectedly. Always decide upfront which fields need deep copying and which can be shared. Document your decisions clearly.