Prototype - Pattern Clarity #10
Ivan Vydrin
Software Engineer | .NET & Azure Professional | AI/ML Enthusiast | Crafting Scalable and Resilient Solutions
The Prototype design pattern allows you to create new objects by cloning existing instances, avoiding the overhead of creating them from scratch and avoiding the dependency between classes. This approach is particularly handy when the object creation process is expensive or complex, and you need to produce multiple instances that share the same initial configuration.
? Problem
Imagine you're developing a graphics editor. You have various shapes - circles, rectangles, polygons - each with multiple properties like color, border thickness, gradients, etc. To create a new shape, you traditionally:
// Existing circle with many properties
var existingCircle = new Circle
{
Color = "Red",
BorderThickness = 2,
Radius = 10,
Gradient = new Gradient("Linear", new[] {0xFF0000, 0x00FF00})
};
// Manually creating a new circle with the same attributes
var newCircle = new Circle
{
Color = existingCircle.Color,
BorderThickness = existingCircle.BorderThickness,
Radius = existingCircle.Radius,
// Notice we must also create a new Gradient object to avoid reference issues
Gradient = new Gradient(
existingCircle.Gradient.Type,
existingCircle.Gradient.ColorStops
)
};
// This approach is error-prone, especially if new properties or nested objects are added
Over time, this approach causes problems:
? Solution
The Prototype pattern suggests creating new objects by cloning a prototypical instance. Each "prototype" serves as a blueprint. When you need a new object, you simply clone the prototype instead of recreating every detail manually.
Key elements include:
With this pattern, you typically keep a registry (or just a reference) of prototypes you frequently need. Whenever you need a copy, you call prototype.Clone(), and the rest happens internally.
?? Use Cases
?? Benefits & Drawbacks
Pros:
Cons:
?? How to Implement
?? Handling Different Copy Depths
Choose which strategy works best based on how you plan to use the clones.
Prototype Registry
The Prototype Registry is a convenient tool for managing commonly used prototypes by maintaining a collection of pre-configured objects that can be quickly duplicated. A basic implementation could use a simple map from names to prototypes, but if you need more advanced ways to find the right prototype, you can enhance the registry to support richer search capabilities.
?? Handling Concurrency
Check out my article about thread-safety in .NET.
?? Code Example
Below is a simplified C# example that demonstrates cloning a shape prototype using Prototype Registry. We have an abstract Shape class and two concrete shapes (Circle and Rectangle). Each implements a Clone() method that duplicates its state.
namespace PatternClarity.CreationalPatterns.Prototype;
// Abstract prototype
abstract class Shape
{
public string Color { get; set; }
public int BorderThickness { get; set; }
public Shape(string color, int borderThickness)
{
Color = color;
BorderThickness = borderThickness;
}
// Prototype method
public abstract Shape Clone();
}
// Concrete prototype 1
class Circle : Shape
{
public int Radius { get; set; }
public Circle(string color, int borderThickness, int radius)
: base(color, borderThickness)
{
Radius = radius;
}
// Shallow copy; adapt for deeper structures if necessary
public override Shape Clone()
{
return (Shape)this.MemberwiseClone();
}
public override string ToString()
{
return $"Circle [Color={Color}, Thickness={BorderThickness}, Radius={Radius}]";
}
}
// Concrete prototype 2
class Rectangle : Shape
{
public int Width { get; set; }
public int Height { get; set; }
public Rectangle(string color, int borderThickness, int width, int height)
: base(color, borderThickness)
{
Width = width;
Height = height;
}
// Shallow copy; adapt for deeper structures if necessary
public override Shape Clone()
{
return (Shape)this.MemberwiseClone();
}
public override string ToString()
{
return $"Rectangle [Color={Color}, Thickness={BorderThickness}, Width={Width}, Height={Height}]";
}
}
// The Prototype Registry
class PrototypeRegistry
{
private readonly Dictionary<string, Shape> _prototypes = new Dictionary<string, Shape>();
public void RegisterPrototype(string key, Shape prototype)
{
_prototypes[key] = prototype;
}
public Shape CreateShape(string key)
{
if (_prototypes.ContainsKey(key))
{
return _prototypes[key].Clone();
}
throw new ArgumentException($"No prototype registered under key: {key}");
}
}
class Program
{
static void Main()
{
// Initialize the registry
var registry = new PrototypeRegistry();
// Create prototypes
Circle circlePrototype = new("Red", 2, 10);
Rectangle rectanglePrototype = new("Blue", 1, 20, 10);
// Register the prototypes
registry.RegisterPrototype("red-circle", circlePrototype);
registry.RegisterPrototype("blue-rectangle", rectanglePrototype);
// Retrieve and clone shapes using the registry
Shape clonedCircle1 = registry.CreateShape("red-circle");
Shape clonedCircle2 = registry.CreateShape("red-circle");
Shape clonedRectangle = registry.CreateShape("blue-rectangle");
// Modify one clone's properties
(clonedCircle1 as Circle).Radius = 15;
clonedCircle1.Color = "Green";
// Output to show differences
Console.WriteLine($"Original Circle Prototype: {circlePrototype}");
Console.WriteLine($"Cloned Circle 1: {clonedCircle1}");
Console.WriteLine($"Cloned Circle 2: {clonedCircle2}");
Console.WriteLine($"Cloned Rectangle: {clonedRectangle}");
}
}
Console output:
Original Circle Prototype: Circle [Color=Red, Thickness=2, Radius=10]
Cloned Circle 1: Circle [Color=Green, Thickness=2, Radius=15]
Cloned Circle 2: Circle [Color=Red, Thickness=2, Radius=10]
Cloned Rectangle: Rectangle [Color=Blue, Thickness=1, Width=20, Height=10]
?? Notice that modifying one clone does not affect the prototype or the other clones, demonstrating how each cloned object is an independent instance.
Conclusion
Prototype is a powerful pattern to reduce the complexity and cost of creating new objects. Instead of repeating the same initialization code, you clone a prototypical instance. This pattern is invaluable when you have objects that are difficult or expensive to create, or when you want a flexible way to generate new instances of many possible classes without binding your code to their concrete constructors.
Carefully manage your prototypes, especially with nested objects or when prototypes change at runtime. When implemented correctly, the Prototype pattern streamlines object creation, making your code more maintainable and efficient.
Thank you for reading this article in the "Pattern Clarity" series! I'd love to hear your thoughts and experiences with the Prototype pattern - share your comments and suggestions. Let's keep the conversation going!