Last updated on July 18, 2021
This post will demonstrate the Prototype design pattern – another pattern for creating instances of objects.
This pattern creates new objects, by “cloning” them from a single (prototype) object.
Examples
If we were writing a role-playing game, we might want to create multiple instances of monsters for the player to fight.
Non-Pattern Version
In the non-prototype version, is we want a new Monster object, we need to call the constructor. If we don’t, and just set a new Monster variable equal to the first Monster object, both variables will be pointing to (referencing) the same object.
Monster.cs
namespace Engine.PrototypePattern.NonPatternVersion
{
public class Monster
{
public string Name { get; private set; }
public int HitPoints { get; private set; }
public Monster(string name, int hitPoints)
{
Name = name;
HitPoints = hitPoints;
}
public void ApplyDamage(int amountOfDamage)
{
HitPoints -= amountOfDamage;
}
}
}
TestMonster.cs
using Engine.PrototypePattern.NonPatternVersion;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace TestEngine.PrototypePattern.NonPatternVersion
{
[TestClass]
public class TestMonster
{
[TestMethod]
public void Test_ReferenceProblem()
{
Monster spider1 = new Monster("Giant Spider", 10);
Monster spider2 = spider1;
Assert.AreEqual(10, spider1.HitPoints);
Assert.AreEqual(10, spider2.HitPoints);
spider2.ApplyDamage(2);
// Even though we only called ApplyDamage on spider2,
// the HitPoints for both spider objects is 8.
//
// This is because the spider variables are pointing to (referencing)
// a single spider object - the original spider1.
Assert.AreEqual(8, spider1.HitPoints);
Assert.AreEqual(8, spider2.HitPoints);
}
[TestMethod]
public void Test_ReferenceProblemSolution()
{
Monster spider1 = new Monster("Giant Spider", 10);
Monster spider2 = new Monster("Giant Spider", 10);
Assert.AreEqual(10, spider1.HitPoints);
Assert.AreEqual(10, spider2.HitPoints);
spider2.ApplyDamage(2);
// There is no reference problem,
// because we created spider2 by calling the Monster constructor.
Assert.AreEqual(10, spider1.HitPoints);
Assert.AreEqual(8, spider2.HitPoints);
}
}
}
In the first test (Test_ReferenceProblem), we create “standardGiantSpider” – the “base” giant spider object.
When we create a new spider variable, and set it to standardGiantSpider, it will not be a separate object. Instead, it will “reference” the original standardGiantSpider object.
Anything change done to one spider object, appears on all spider objects, because they are all pointing to a single object.
To prevent this problem, without using the Prototype design pattern, we would need to call the constructor for every Monster object we create – as in the second test (Test_ReferenceProblemSolution).
Pattern Version – Simple
Monster.cs
namespace Engine.PrototypePattern.PatternVersion_Simple
{
public class Monster
{
public string Name { get; private set; }
public int HitPoints { get; private set; }
public Monster(string name, int hitPoints)
{
Name = name;
HitPoints = hitPoints;
}
public void ApplyDamage(int amountOfDamage)
{
HitPoints -= amountOfDamage;
}
public Monster Clone()
{
return new Monster(Name, HitPoints);
}
}
}
The constructor is called the first time, to create a prototype giant spider object. To create more giant spider objects, we will call a “Clone()” method on the prototype giant spider object. This will create completely new objects, because the Clone method calls the Monster constructor.
TestMonster.cs
using Engine.PrototypePattern.PatternVersion_Simple;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace TestEngine.PrototypePattern.PatternVersion_Simple
{
[TestClass]
public class TestMonster
{
[TestMethod]
public void Test_PrototypePattern()
{
Monster standardGiantSpider = new Monster("Giant Spider", 10);
// Calling the Clone method is how the Prototype Pattern is implemented.
Monster spider2 = standardGiantSpider.Clone();
Assert.AreEqual(10, standardGiantSpider.HitPoints);
Assert.AreEqual(10, spider2.HitPoints);
spider2.ApplyDamage(2);
// There is no reference problem,
// because the Clone method constructs a new Monster object.
Assert.AreEqual(10, standardGiantSpider.HitPoints);
Assert.AreEqual(8, spider2.HitPoints);
}
}
}
In the second test (Test_PrototypePattern), we call the Clone method on the initial standardGiantSpider object. Because the Clone method return a “new Monster”, the additional spider variables are referencing individual, unique objects. Anything done to one spider variable will not affect the other spider variables.
Pattern Version – More Complex
Here is a more practical scenario for using the Prototype design pattern.
In this version of the Monster class, the constructor loads a LootTable list. These are items the Monster could have in its inventory, with the percentage of it having that item.
In a real program, we would probably load this from the database – although, for this example, I manually populate the LootTable with LootTableEntry values.
If we called the constructor every time we created a Monster object, it would need to re-run the database call. So, in this example, the Clone method calls a private constructor that populates the LootTable property with the values from the prototype’s LootTable property.
Monster.cs
using System.Collections.Generic;
namespace Engine.PrototypePattern.PatternVersion_Complex
{
public class Monster
{
public string Name { get; private set; }
public int HitPoints { get; private set; }
public List<LootTableEntry> LootTable { get; set;}
// Public constructor, called to create the prototype Monster object.
public Monster(string name, int hitPoints)
{
Name = name;
HitPoints = hitPoints;
// In this part, pretend we are populating LootTable using a database query.
LootTable = new List<LootTableEntry>();
LootTable.Add(new LootTableEntry { ItemID = 1, DropPercentage = 10 });
LootTable.Add(new LootTableEntry { ItemID = 2, DropPercentage = 5 });
LootTable.Add(new LootTableEntry { ItemID = 5, DropPercentage = 1 });
LootTable.Add(new LootTableEntry { ItemID = 12, DropPercentage = 50 });
LootTable.Add(new LootTableEntry { ItemID = 27, DropPercentage = 33 });
LootTable.Add(new LootTableEntry { ItemID = 42, DropPercentage = 100 });
}
// Private constructor called by Clone method.
// Does not need to connect to the database to populate the LootTable property.
private Monster(string name, int hitPoints, List<LootTableEntry> lootTable)
{
Name = name;
HitPoints = hitPoints;
LootTable = lootTable;
}
public void ApplyDamage(int amountOfDamage)
{
HitPoints -= amountOfDamage;
}
public Monster Clone()
{
// This version of Clone calls the private constructor,
// to prevent re-running the database query to populate LootTable.
return new Monster(Name, HitPoints, LootTable);
}
}
}
LootTableEntry.cs
namespace Engine.PrototypePattern.PatternVersion_Complex
{
public class LootTableEntry
{
public int ItemID { get; set; }
public int DropPercentage { get; set; }
}
}
In both these scenarios, we could have created the Monster objects using the Factory design pattern. The Prototype design pattern is just another “tool in your toolbox” – to use when it seems appropriate.
Where I’ve found it useful
I usually do not need to use this in my business applications. However, I have used it in a game – just like in the “Better Example” version.
This pattern is most useful when you’ll need to create multiple instances of an objects, and the constructor has a lot of initialization to perform. With a prototype, you only need to do that initialization once.
Source code for my design pattern lessons