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