| Type | Creational |
| Purpose | Specify the kinds of objects to create using a prototypical instance, and create new objects by copying the prototype. |
Description
The Prototype pattern creates new objects by cloning existing prototypes. Instead of constructing and initializing a new object, the system copies an existing object that already embodies the desired state.
Prototype offers an alternative to creating objects through constructors or factories. This can be useful when duplicating documents, managing configuration instances, spawning game entities, or creating reusable preconfigured components.
Prototype is useful when object construction is expensive, when many instances share the same initial state, or when clients shouldn't need to know how an object is constructed.
Prototype is primarily concerned with creating new objects by copying existing ones.
Overview
| Prototype | Defines an interface that says prototypes must be cloneable. |
| ConcretePrototypes | Classes with implementations of the Prototype interface. |
| Client | A class that uses the Prototype Interface to clone an object. |
Implementation
In C#, there is a MemberwiseClone method available to all objects, but unfortunately it only performs a shallow copy. Some developers distinguish between shallow and deep cloning operations. I prefer cloning to have consistent semantics so that objects are always copied into the expected state. Here, Clone, delegates to DeepClone, allowing the class to implement ICloneable while also providing a strongly typed cloning operation that avoids client-side casting.
ICloneable is somewhat controversial because it doesn't define whether cloning should be shallow or deep. In this example, I implement it as a deep copy so the behaviour is predictable.
Monster Spawning
Imagine you're writing a game that frequently spawns Goblins, Orcs and Bugbears. Constructing each monster from scratch quickly becomes repetitive. Instead, keep one configured instance of each creature and clone it whenever you need another.
Each monster in the patrol starts with identical statistics, but they're independent objects. If one Goblin takes damage, the others remain unaffected.
This is exactly the kind of problem the Prototype pattern was designed to solve.
// Prototype
public interface IPrototype<out T> : ICloneable
{
T DeepClone();
}
// ConcretePrototype
public class Monster : IPrototype<Monster>
{
public string Name { get; set; } = "";
public int Strength { get; set; }
public int Dexterity { get; set; }
public int Constitution { get; set; }
public int Intelligence { get; set; }
public int Wisdom { get; set; }
public int Charisma { get; set; }
public int HitPoints { get; set; }
public int ArmorClass { get; set; }
public int Speed { get; set; }
public List<string> Skills { get; set; } = [];
public List<string> Senses { get; set; } = [];
public List<string> Attacks { get; set; } = [];
public object Clone() => DeepClone();
public Monster DeepClone()
{
var monster = new Monster
{
Name = Name,
Strength = Strength,
Dexterity = Dexterity,
Constitution = Constitution,
Intelligence = Intelligence,
Wisdom = Wisdom,
Charisma = Charisma,
HitPoints = HitPoints,
ArmorClass = ArmorClass,
Speed = Speed,
Skills = [..Skills],
Senses = [..Senses],
Attacks = [..Attacks]
};
return monster;
}
}
public static class PrototypeDemo
{
private static readonly Random Random = new();
// More ConcretePrototypes
private static readonly Monster Goblin = new()
{
Name = "Goblin",
Strength = 8,
Dexterity = 14,
Constitution = 10,
Intelligence = 10,
Wisdom = 8,
Charisma = 8,
HitPoints = 7,
ArmorClass = 15,
Speed = 30,
Skills = ["Stealth", "Nimble Escape", "Sneak Attack"],
Senses = ["Darkvision 60ft.", "Passive Perception 9"],
Attacks = ["Scimitar", "Shortbow"]
};
private static readonly Monster GoblinBoss = new()
{
Name = "Goblin Boss",
Strength = 10,
Dexterity = 14,
Constitution = 13,
Intelligence = 10,
Wisdom = 10,
Charisma = 15,
HitPoints = 21,
ArmorClass = 17,
Speed = 30,
Skills = ["Stealth", "Nimble Escape", "Sneak Attack"],
Senses = ["Darkvision 60ft.", "Passive Perception 10"],
Attacks = ["Scimitar", "Javelin", "Multiattack"]
};
private static readonly Monster Orc = new()
{
Name = "Orc",
Strength = 16,
Dexterity = 12,
Constitution = 16,
Intelligence = 7,
Wisdom = 11,
Charisma = 10,
HitPoints = 15,
ArmorClass = 13,
Speed = 30,
Skills = ["Intimidation"],
Senses = ["Darkvision 60ft.", "Passive Perception 10"],
Attacks = ["Greataxe", "Javelin"]
};
private static readonly Monster Bugbear = new()
{
Name = "Bugbear",
Strength = 16,
Dexterity = 14,
Constitution = 15,
Intelligence = 8,
Wisdom = 11,
Charisma = 9,
HitPoints = 27,
ArmorClass = 16,
Speed = 30,
Skills = ["Stealth", "Surprise Attack"],
Senses = ["Darkvision 60ft.", "Passive Perception 11"],
Attacks = ["Morningstar", "Javelin"]
};
// Demonstration
public static void Execute()
{
var patrol = GetRandomPatrol();
Console.WriteLine("Uh oh... you bump into a large patrol of monsters!");
Console.WriteLine();
Console.WriteLine("The patrol consists of:");
Console.WriteLine();
foreach (var monster in patrol)
{
Console.WriteLine($"{monster.Name} with {monster.HitPoints} hit points");
}
Console.WriteLine();
Console.WriteLine("You still have time to flee!");
}
private static List<Monster> GetRandomPatrol()
{
List<Monster> patrol = [];
for (var i = 0; i < 7; ++i)
{
var monster = Random.Next(10) switch
{
< 5 => Goblin,
< 8 => Orc,
_ => Bugbear
};
var baddie = monster.DeepClone();
patrol.Add(baddie);
}
var boss = GoblinBoss.DeepClone();
patrol.Add(boss);
return patrol;
}
}
// outputs:
// Uh oh... you bump into a large patrol of monsters!
// The patrol consists of:
// Goblin with 7 hit points
// Goblin with 7 hit points
// Orc with 15 hit points
// Bugbear with 27 hit points
// Goblin with 7 hit points
// Goblin with 7 hit points
// Orc with 15 hit points
// Goblin Boss with 21 hit points
// You still have time to flee!
Notes
If you're not concerned with casting, you can drop the Prototype interface and just use ICloneable:
public class Monster : ICloneable
{
public object Clone()
{
// ...
}
}
Possible Alternative
For more complex state, or a generic deep-copy mechanism, JSON serialization is an option:
using Newtonsoft.Json;
public static class Spawner
{
public static T Spawn<T>(T instance)
{
var json = JsonConvert.SerializeObject(instance);
return JsonConvert.DeserializeObject<T>(json)
?? throw new InvalidOperationException("Unable to clone object.");
}
}
This approach is simple to implement but incurs the overhead of serialization and requires all state to be serializable. For tooling or prototypes, serialization can be an easy way to perform a deep copy. For performance-sensitive code (such as games), explicit cloning is usually preferable.
Summary
There are variations when implementing this pattern. I highly recommend the demonstration and discussion found here, in Robert Nystrom's awesome book Game Programming Patterns, readable online!
For more information, please see here.