Skip to main content

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
Always Deep Copy Reference Types

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​

ApproachCopy DepthNotes
MemberwiseClone()Shallowprotected method on object β€” only accessible within the class
ICloneable.Clone()Implementation-dependentAmbiguous β€” doesn't specify shallow vs deep. Avoid in public APIs.
Manual deep copyDeepMost reliable β€” copy each field explicitly
Serialization (JSON/Binary)DeepGeneric but slower β€” serialize then deserialize
record with withShallow (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#, record types with with expressions provide a clean, built-in prototype mechanism
  • ICloneable is ambiguous (shallow or deep?) β€” avoid it in public APIs; prefer a named DeepClone() 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.

  • Builder β€” construct complex objects step-by-step
  • Structs & Records β€” record types with with for modern prototyping
  • Collections β€” cloning collections (reference vs value semantics)