Prototype Pattern
Definitionβ
The Prototype pattern creates new objects by cloning an existing object (the prototype) instead of creating them from scratch. This is useful when object creation is expensive, or when you need copies of objects configured at runtime.
Coffee Shop Exampleβ
A coffee shop has a menu of template coffees β pre-configured base recipes. When a customer orders, the barista clones a template and customizes the copy. This way, the original template stays intact for future orders.
Structureβ
Implementationβ
public interface ICoffeePrototype
{
ICoffeePrototype Clone();
}
public class CoffeeTemplate : ICoffeePrototype
{
public string Name { get; set; } = "";
public double BasePrice { get; set; }
public List<string> Ingredients { get; set; } = new();
public CupSize Size { get; set; } = CupSize.Medium;
public Dictionary<string, double> Extras { get; set; } = new();
// Shallow copy β references are shared!
public ICoffeePrototype Clone() => (ICoffeePrototype)MemberwiseClone();
// Deep copy β everything is duplicated
public CoffeeTemplate DeepClone()
{
return new CoffeeTemplate
{
Name = Name,
BasePrice = BasePrice,
Ingredients = new List<string>(Ingredients),
Size = Size,
Extras = new Dictionary<string, double>(Extras)
};
}
public double TotalPrice => BasePrice + Extras.Values.Sum();
public override string ToString() =>
$"{Name} ({Size}) β ${TotalPrice:F2} [{string.Join(", ", Ingredients)}]" +
(Extras.Count > 0 ? $" + {string.Join(", ", Extras.Keys)}" : "");
}
Usage β Menu Templatesβ
// Create prototype templates (the "menu")
var latteTemplate = new CoffeeTemplate
{
Name = "Latte",
BasePrice = 4.00,
Ingredients = new() { "Espresso Shot", "Steamed Milk" },
Size = CupSize.Medium,
};
var mochaTemplate = new CoffeeTemplate
{
Name = "Mocha",
BasePrice = 4.50,
Ingredients = new() { "Espresso Shot", "Steamed Milk", "Chocolate Syrup" },
Extras = new() { { "Whipped Cream", 0.50 } },
};
// Clone and customize for each order
var order1 = latteTemplate.DeepClone();
order1.Size = CupSize.Large;
order1.Extras["Oat Milk"] = 0.60;
var order2 = mochaTemplate.DeepClone();
order2.Ingredients.Add("Caramel Drizzle");
order2.Extras["Extra Shot"] = 0.75;
var order3 = latteTemplate.DeepClone(); // fresh copy, untouched by order1
Console.WriteLine(order1); // Latte (Large) β $4.60 [Espresso Shot, Steamed Milk] + Oat Milk
Console.WriteLine(order2); // Mocha (Medium) β $5.75 [Espresso Shot, Steamed Milk, Chocolate Syrup, Caramel Drizzle] + Whipped Cream, Extra Shot
Console.WriteLine(order3); // Latte (Medium) β $4.00 [Espresso Shot, Steamed Milk]
The Danger of Shallow Copyβ
var template = new CoffeeTemplate
{
Name = "Latte",
Ingredients = new() { "Espresso Shot", "Steamed Milk" },
};
// Shallow copy β Ingredients list is SHARED
var shallowCopy = (CoffeeTemplate)template.Clone();
shallowCopy.Ingredients.Add("Vanilla Syrup");
// The original template is now contaminated!
Console.WriteLine(template.Ingredients.Count); // 3 β BUG!
Console.WriteLine(shallowCopy.Ingredients.Count); // 3
// Deep copy β Ingredients list is INDEPENDENT
var deepCopy = template.DeepClone();
deepCopy.Ingredients.Add("Caramel");
// Original is safe
Console.WriteLine(template.Ingredients.Count); // 3 (already contaminated)
Console.WriteLine(deepCopy.Ingredients.Count); // 4
MemberwiseClone() copies references, not the objects they point to. List<T>, Dictionary<K,V>, arrays, and custom objects are all shared between the original and the clone. Always implement deep copy for reference-type properties.
.NET Cloning Approachesβ
| Approach | Copy Depth | Notes |
|---|---|---|
MemberwiseClone() | Shallow | protected method on object β only accessible within the class |
ICloneable.Clone() | Implementation-dependent | Ambiguous β doesn't specify shallow vs deep. Avoid in public APIs. |
| Manual deep copy | Deep | Most reliable β copy each field explicitly |
| Serialization (JSON/Binary) | Deep | Generic but slower β serialize then deserialize |
record with with | Shallow (with deep for value types) | Modern C# β best for immutable data |
Serialization-Based Deep Cloneβ
using System.Text.Json;
public static class CloningExtensions
{
public static T DeepClone<T>(this T source) where T : class
{
var json = JsonSerializer.Serialize(source);
return JsonSerializer.Deserialize<T>(json)!;
}
}
// Usage
var order = latteTemplate.DeepClone();
Record-Based Prototypeβ
public record CoffeeOrder(
string Name,
double Price,
CupSize Size = CupSize.Medium,
string MilkType = "Whole"
)
{
// The 'with' expression creates a shallow copy with modifications
public CoffeeOrder Customize(CupSize? size = null, string? milk = null)
=> this with
{
Size = size ?? Size,
MilkType = milk ?? MilkType
};
}
// Usage
var baseLatte = new CoffeeOrder("Latte", 4.00);
var customized = baseLatte.Customize(CupSize.Large, "Oat");
// baseLatte is untouched: Latte (Medium, Whole)
// customized is new: Latte (Large, Oat)
When to Useβ
- Object creation is expensive (heavy initialization, DB calls, computation)
- You need many similar objects that differ only slightly
- Objects are configured at runtime and you want to preserve that configuration
- You want to avoid building a parallel factory hierarchy
When NOT to Useβ
- Objects are simple and cheap to create β just use constructors
- All fields are value types β
MemberwiseClone()works, but a copy constructor is clearer - You need deep copies of complex object graphs β serialization-based cloning may be slow
- The class hierarchy changes frequently β maintaining
Clone()across subclasses is tedious
Key Takeawaysβ
- Prototype clones an existing object instead of creating from scratch
- Shallow copy shares reference-type fields β almost always a bug for mutable objects
- Deep copy duplicates everything β implement manually or use serialization
- In modern C#,
recordtypes withwithexpressions provide a clean, built-in prototype mechanism ICloneableis ambiguous (shallow or deep?) β avoid it in public APIs; prefer a namedDeepClone()method
Interview Questionsβ
Q: What's the difference between shallow copy and deep copy? Shallow copy duplicates the object but shares references to nested objects. Deep copy duplicates everything recursively β the clone is fully independent.
Q: Why is ICloneable considered problematic?
Its Clone() method doesn't specify shallow or deep copy. Callers can't know what they're getting. Prefer explicit DeepClone() / ShallowClone() methods.
Q: When is Prototype better than Factory Method? When object creation is expensive and you have a configured instance to copy. Prototype avoids repeating expensive setup. Factory Method creates from scratch every time.
Related Topicsβ
- Builder β construct complex objects step-by-step
- Structs & Records β
recordtypes withwithfor modern prototyping - Collections β cloning collections (reference vs value semantics)