Prototype - Pattern Clarity #10
Prototype - Creational Pattern

Prototype - Pattern Clarity #10

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:

  1. Instantiate a new shape class.
  2. Manually copy or set all properties and configurations.
  3. Deal with intricate details (like copying nested structures).

 // 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:

  1. Complex initialization: if the shape has many properties or nested objects, creating and configuring a new instance becomes cumbersome.
  2. Hard to maintain: adding or changing properties forces you to hunt down all the places where new instances are created, leading to fragile code.
  3. Performance overhead: constructing a shape with complex configuration (e.g., reading from resources or doing expensive computations) repeatedly can be slow.
  4. Error-prone: missing a property while replicating can lead to subtle bugs.


? 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:

  • Prototype interface: typically defines a Clone() method or similar, which returns a copy of the object.
  • Concrete prototypes: implement the cloning logic (shallow or deep copy) for their properties.
  • Client: requests new objects by cloning existing prototypes, without worrying about the internal creation details.

Prototype class diagram.

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

  • Complex object creation: if an object takes a long time or requires many steps to initialize, cloning an already-initialized object can be more efficient.
  • Object pools: in gaming or real-time systems, creating certain entities repeatedly is costly - cloning pre-initialized prototypes can speed things up.
  • UI or document templates: duplicating layout elements or sections of a document that already have the desired settings.
  • When subclass instantiation is tricky: if your system has multiple subclasses and you want to create copies without specifying concrete classes each time.


?? Benefits & Drawbacks

Pros:

  • Reduced creation cost: clone expensive objects quickly instead of reconstructing them.
  • Less complex code: avoid re-implementing initialization logic for each new instance.
  • Decoupling: clients work with the prototype interface, not concrete constructors or classes.
  • Runtime flexibility: you can register or remove prototypes on the fly, controlling which types are available.

Cons:

  • Cloning complexity: you must carefully implement clone methods, especially if deep copies are required (handling nested objects, references, etc.).
  • Potential overhead: maintaining a prototype registry or large sets of prototypes might add storage overhead if you store too many.
  • Hard to track changes: if the prototype is mutable, changes to it can affect all future clones in unexpected ways.


?? How to Implement

  1. Define the Prototype interface: a Clone() (or Copy() / DeepCopy()) method that returns a new object of the same type.
  2. Create Concrete Prototypes: each class implements how it copies its properties. Decide between shallow or deep copy.
  3. Set up a Prototype Registry (optional): store commonly used prototypes in a lookup table, keyed by an identifier.
  4. Use Clones: whenever you need a new instance, clone an existing prototype instead of calling a constructor.

?? Handling Different Copy Depths

  • Shallow copy: only clones the top-level properties, copying references to any nested objects.
  • Deep copy: recursively clones all nested objects. More thorough, but can be more complex and expensive.

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.

Prototype with PrototypeRegistry class diagram.

?? Handling Concurrency

  • Immutable prototypes: if your prototype objects never change, concurrency issues are minimal since reading shared immutable data is safe.
  • Synchronized cloning: if prototypes are mutable, be careful with concurrent access. Use locks or other synchronization when cloning.

Check out my article about thread-safety in .NET.

  • Distributed environments: if prototypes live across multiple services, you'll need a shared approach, possibly serializing and transferring the prototype data over the network.


?? 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!

要查看或添加评论,请登录

Ivan Vydrin的更多文章

  • Types of Machine Learning

    Types of Machine Learning

    Machine Learning (ML) is a subset of Artificial Intelligence (AI) that enables computer systems to learn from data and…

    2 条评论
  • Chain of Responsibility - Pattern Clarity #9

    Chain of Responsibility - Pattern Clarity #9

    The Chain of Responsibility design pattern allows you to pass a request through a series of handlers (objects), where…

  • Thread Safety in .NET: lock, Semaphore and Mutex

    Thread Safety in .NET: lock, Semaphore and Mutex

    Thread Safety matters: in multi-threaded .NET applications, multiple threads often access shared data concurrently.

    3 条评论
  • Vector Databases in AI/ML: the next-gen infrastructure for intelligent search

    Vector Databases in AI/ML: the next-gen infrastructure for intelligent search

    Traditional databases struggle to handle AI-generated data like images, text, and audio embeddings. These…

    1 条评论
  • State - Pattern Clarity #8

    State - Pattern Clarity #8

    The State design pattern allows an object to alter its behavior when its internal state changes, making the object…

    4 条评论
  • Agentic AI: the rise of autonomous agents

    Agentic AI: the rise of autonomous agents

    Artificial Intelligence is evolving from simple tools into agentic systems that can act with autonomy and purpose…

    6 条评论
  • Singleton - Pattern Clarity #7

    Singleton - Pattern Clarity #7

    The Singleton design pattern ensures that a class has only one instance while providing a global point of access to it.…

    1 条评论
  • Exploring Types of Chatbots: an AI/ML perspective

    Exploring Types of Chatbots: an AI/ML perspective

    Chatbots have become an essential part of modern digital interactions, transforming customer service, sales, education,…

  • Observer - Pattern Clarity #6

    Observer - Pattern Clarity #6

    The Observer behavioral pattern establishes a one-to-many dependency between objects, so that when one object (the…

    4 条评论
  • Let's build a Free Web Chat Bot - Part 2/2

    Let's build a Free Web Chat Bot - Part 2/2

    In today's fast-moving digital world, smart and intuitive conversations give businesses a real edge. Azure AI Services…

社区洞察