Prototype Pattern in C#
Kostas Kalafatis
Posted on April 13, 2023
The Prototype is a creational design pattern that lets us copy existing objects without making our code dependent on our classes.
The typical way of thinking about this pattern is to consider how we might model the colour spectrum. There are something like 10 million visible colours, so modelling them as individual classes (e.g. Red
, LightMauve
, Skobeloff
, Macaroni and Cheese
, Zomp
) would be rather impractical.
However, a colour is a colour, no matter what colour it is; colours have the same properties as each other even though they don't have the same values. If we needed to create a lot of colour instances, we could do so by using the Prototype design pattern.
Conceptualizing the Problem
Say we have an object we want to create an exact copy of. How would we do it? First, we have to create a new object of the same class. Then we have to go through all the fields of the original instance and copy their values to the new one.
Excellent. But there's a catch. Not all objects can be copied that way because some of the object's fields may be private and not visible from outside of the object itself.
There's one more problem with the direct approach. Since we have to know the object's class to create a duplicate, our code becomes dependent on that class. If the extra dependency doesn't scare you there's another catch. Often, we only know the interface the object implements, but not its concrete class.
The Prototype pattern delegates the cloning process to the objects that are being cloned. The pattern declares a common interface for all objects that support cloning. This interface lets us clone an instance without coupling our code with its class. More often than not, such an interface contains only a single Clone
method.
The implementation of the Clone
method is very similar in all classes. The method creates a new instance of the current class. Then it carries over all of the values of the old instance into the new one.
An object that supports cloning is called a prototype. When our objects have dozens of fields and hundreds of possible configurations, cloning them might serve as an an alternative to subclassing. Here's how it works: We create a set of objects, configured in various ways. When we need an object configured in a specific way, we just clone a prototype instead of constructing a new instance from scratch.
Structuring the Prototype
The prototype pattern has two popular flavors: The Basic Prototype implementation, and the Prototype Registry implementation:
Basic Prototype Implementation
In its base implementation, the Prototype pattern has 3 participants:
-
Prototype: The Prototype interface declares the cloning methods. In most cases, it's a single
Clone
method, but it can be more complicated depending on the context. - Concrete Prototype: The cloning method is implemented by the Concrete Prototype class. This method may handle several edge scenarios of the cloning process such as cloning linked objects, untangling recursive dependencies, and so on, in addition to transferring the original object's contents to the clone.
- Client: The Client can produce a copy of any object that implements the prototype interface.
Prototype Registry Implementation
In the base implementation, the Prototype Registry pattern has 4 participants:
-
Prototype: The Prototype interface declares the cloning methods. In most cases, it's a single
Clone
method, but it can be more complicated depending on the context. - Concrete Prototype: The cloning method is implemented by the Concrete Prototype class. This method may handle several edge scenarios of the cloning process such as cloning linked objects, untangling recursive dependencies, and so on, in addition to transferring the original object's contents to the clone.
- Prototype Registry: The Prototype Registry makes it simple to find regularly used prototypes. It keeps a collection of ready-to-copy pre-built items. A name prototype hash map is the most basic prototype registry. If you require more search parameters than just a name, you may create a far more sophisticated version of the registry.
- Client: The Client can produce a copy of any object that implements the prototype interface.
To demonstrate how the Prototype pattern works we are going to create a system that generates different shapes.
First, we'll create an AbstractShape
class (the Prototype participant) to represent a shape, and define a method Clone()
which an instance of this class can use to clone itself:
/// <summary>
/// The Prototype abstract class
/// </summary>
public abstract class Shape
{
public int x;
public int y;
public string color;
public Shape()
{
}
public Shape(Shape target)
{
if (target != null)
{
this.x = target.x;
this.y = target.y;
this.color = target.color;
}
}
public abstract Shape clone();
public override bool Equals(object object2)
{
if (!(object2 is Shape)) return false;
Shape shape2 = (Shape)object2;
return shape2.x == x && shape2.y == y && object.Equals(shape2.color, color);
}
}
Now that we have our abstract class, we will implement our Concrete Prototypes. The first concrete prototype will create a circle. It will handle cloning and equality comparison.
public class Circle : Shape
{
public int Radius;
public Circle()
{
}
public Circle(Circle target) : base(target)
{
if (target != null)
{
this.Radius = target.Radius;
}
}
public override Shape clone()
{
return new Circle(this);
}
public override bool Equals(object object2)
{
if (!(object2 is Circle shape2) || !base.Equals(shape2)) return false;
return shape2.Radius == Radius;
}
public override int GetHashCode()
{
var hashCode = base.GetHashCode();
hashCode = hashCode * 31 + Radius.GetHashCode();
return hashCode;
}
}
The second concrete prototype will create a rectangle (oh how original!). Again, it will handle cloning and equality comparisons.
public class Rectangle : Shape
{
public int Width;
public int Height;
public Rectangle()
{
}
public Rectangle(Rectangle target) : base(target)
{
if (target != null)
{
this.Width = target.Width;
this.Height = target.Height;
}
}
public override Shape clone()
{
return new Rectangle(this);
}
public override bool Equals(object object2)
{
if (!(object2 is Rectangle) || !base.Equals(object2)) return false;
Rectangle shape2 = (Rectangle)object2;
return shape2.Width == Width && shape2.Height == Height;
}
public override int GetHashCode()
{
int hashCode = base.GetHashCode();
hashCode = hashCode * 31 + Width.GetHashCode();
hashCode = hashCode * 31 + Height.GetHashCode();
return hashCode;
}
}
Now we need to create a bunch of shapes.
In our Main()
method (which does double-duty as our Client participant), we can do just that, by instantiating the prototype and then cloning it:
public static void Main(string[] args)
{
List<Shape> shapes = new List<Shape>();
List<Shape> shapesCopy = new List<Shape>();
Circle circle = new Circle();
circle.x = 10;
circle.y = 20;
circle.Radius = 15;
circle.color = "red";
shapes.Add(circle);
Circle anotherCircle = (Circle)circle.clone();
shapes.Add(anotherCircle);
Rectangle rectangle = new Rectangle();
rectangle.Width = 10;
rectangle.Height = 20;
rectangle.color = "blue";
shapes.Add(rectangle);
cloneAndCompare(shapes, shapesCopy);
}
private static void cloneAndCompare(List<Shape> shapes, List<Shape> shapesCopy)
{
shapesCopy.AddRange(shapes.Select(shape => shape.clone()));
for (var i = 0; i < shapes.Count; i++)
{
if (shapes[i] != shapesCopy[i])
{
Console.WriteLine(i + ": Shapes are different objects (Exquisite!)");
if (shapes[i].Equals(shapesCopy[i]))
{
Console.WriteLine(i + ": And they are identical (Charming!)");
}
else
{
Console.WriteLine(i + ": But they are not identical (Obscene!)");
}
}
else
{
Console.WriteLine(i + ": Shape objects are the same (Indecorous!)");
}
}
}
If we run our program we will get the following:
Prototype Registry
We may put up a centralized prototype registry (or factory) with a library of pre-defined prototype objects. We could then obtain new objects from the factory by giving their names or other parameters. The manufacturer would look for an adequate prototype, clone it, and send us a duplicate.
using PrototypeRegistry.Registry;
using PrototypeRegistry.Shapes;
public class Program {
public static void Main(string[] args)
{
BundledShapeRegistry cache = new BundledShapeRegistry();
Shape shape1 = cache.Get("Big green circle");
Shape shape2 = cache.Get("Medium blue rectangle");
Shape shape3 = cache.Get("Medium blue rectangle");
if (shape1 != shape2 && !shape1.Equals(shape2))
{
Console.WriteLine("Big green circle != Medium blue rectangle (yay!)");
}
else
{
Console.WriteLine("Big green circle == Medium blue rectangle (booo!)");
}
if (shape2 != shape3)
{
Console.WriteLine("Medium blue rectangles are two different objects (yay!)");
if (shape2.Equals(shape3))
{
Console.WriteLine("And they are identical (yay!)");
}
else
{
Console.WriteLine("But they are not identical (booo!)");
}
}
else
{
Console.WriteLine("Rectangle objects are the same (booo!)");
}
}
}
If we run our program we will get the following:
Pros and Cons of the Prototype Pattern
✔We can clone objects without coupling to their concrete classes. | ❌Cloning complex objects that have circular references might be very tricky. |
✔ We can get rid of repeated initialization code in favor of cloning pre-built prototypes. | |
✔ We can produce complex objects more conveniently. | |
✔We get an alternative to inheritance when dealing with configuration presets for complex objects. |
Relations to Other Patterns
- Many designs begin with the Factory Method (which is less difficult and more adjustable through subclasses) and progress to the Abstract Factory, Prototype, or Builder (which is more versatile but more complicated).
- Abstract Factory classes are frequently built on a collection of Factory Methods, however, the methods in these classes can also be composed using Prototype.
- When you need to preserve copies of Commands in history, Prototype might be helpful.
- Designs that heavily rely on Composite and Decorator can frequently benefit from the use of Prototypes. You can clone complicated structures using the pattern rather than rebuilding them from scratch.
- Because the Prototype is not dependent on inheritance, it does not have any of its disadvantages. Prototype, on the other hand, necessitates a complex initialization of the copied object. The Factory Method is an inheritance-based method that does not require an initialization step.
- Prototype is sometimes a simpler alternative to Memento. This works if the object whose state you wish to save in history is simple and lacks ties to other resources, or if the linkages are simple to re-establish.
- Singletons may be used to implement Abstract Factories, Builders, and Prototypes.
Final Thoughts
In this article, we have discussed what is the Prototype pattern, when to use it and what are the pros and cons of using this design pattern. We then examined some use cases for this pattern and how the Prototype relates to other classic design patterns.
It's worth noting that the Prototype pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.
Posted on April 13, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.