Card image cap

Exploring Design Patterns - Prototype

Design Patterns  • Posted 7 months ago

There will be times when we have to create multiple copies of a given type for serving various clients and while it might seem simple for light weight objects, it might not be the case with heavy objects involving memory or time intense operations.

For example, let's assume the repository classes which do database heavy lifting for us along with some level of domain operations. While such classes are seldom replicated, there might come scenarios when we need to serve such types for multiple clients without stressing out the resources. At such times we make use of the prototype design pattern.

What is a Prototype Pattern?

A prototype can be called as a base or an original model entity, basing on which multiple replicas might exist serving various variations. A prototype pattern is a design solution used when the type of the objects to be created is determined by a prototypical base type; preferably an abstract type which is cloned to produce newer instance variants. This pattern is particularly helpful when creating new objects by means of using the new keyword can be particularly costly by means of time or memory resource complexities.

The Clone method:

A prototype is implemented by having a base abstract type having a definition for a functionality which does the replication operation; preferably called as a clone() method. This methods how the base type shall be replicated in order to form new types. All the subtypes derive and extend from this type in their own variations and all of these types contain the clone() method by inheritance.

At the end of this thread is an actual Client class which consumes these types created. Also there exists a mapper class which maintains a dictionary of all the subtypes for the prototype along with their objects cached for reuse. The client gets the desired variant of the prototype from this Mapper class, which can clone the prototype to any variant required by the client.

A real world example for this approach can be the mitotic division of cells, where the original cell splits itself into two equal child cells with each being a replica of the base cell. And when you look at this approach, this does the job similar to how a Factory pattern does.

It depends on the scenario which decides which of the creational pattern to be used. While the approaches are similar, the factory pattern deals with different variants of types for a condition, while the Prototype calls a prototype which is replicated for the subtypes when required without the need for having to copy at the client level. While the cloning might be a feature offered, one must decide how the copies are made; either a shallow or a deep copy.

Shallow vs Deep copy: which one to clone?

A shallow copy is the simplest, which involves a one-to-one copy of all values into a new replica of the original object. While goes simple for primitive types, the difference is felt when the type contains references to another object type which is a second level of copy to be made. And the shallow copy simply copies the reference on to the new replica and hence when we change the state of the referenced type, the replica changes as well. This is where the deep copy comes into picture.

A deep copy on the other hand, has all the primitive types copied off their values into the new replica and the referenced types are created off with a new instance. This makes sure that the reference type of the new replica has a reference to the same referenced type, but via a new link other than the one in the original type. This makes the state changes in the original type independent of the replica.

When we talk about implicit cloning, whether its MemberwiseClone() in C# or Object.clone() given by the Cloneable interface in Java, they refer to the shallow copies of the replicas being made. In order to have a deep copy of the replica when cloning, we can have the prototype class implement for us, which can be utilized by all of its derived types. Alternatively, we can create a deep copy of a type by marking the type as "serializable" and implementing a binary serialization of the object.

Example - C# with .NET Core:

Consider a simple Prototype base, which needs to be replicated for various models and the client requests for a clone based on a model type attribute. The PrototypeBase class is an abstract type which implements the interface IBasePrototype that defines a method Clone().


namespace netcore3app.Prototype
{
    public interface IBasePrototype
    {
        IBasePrototype Clone();
        string Type { get; }
    }

    public abstract class BasePrototype : IBasePrototype
    {
        protected string _type;

        public IBasePrototype Clone()
        {
            IBasePrototype clone = base.MemberwiseClone() as IBasePrototype;
            return clone;
        }

        public string Type => this._type;
    }
}

Observe that the Clone() method implemented in the abstract base Prototype class makes use of a MemberwiseClone() method from the Object type, which returns a shallow copy of the base type : meaning only the primitive type values are copied onto the returned new replica.

And the BasePrototype is extended by various models of the Prototype; say PrototypeModelOne and PrototypeModelTwo which hold their own value of the ModelType via the constructor.


namespace netcore3app.Prototype
{
    public class PrototypeModelOne : BasePrototype
    {
        public PrototypeModelOne(string type)
        {
            this._type = type;
        }
    }

    public class PrototypeModelTwo : BasePrototype
    {
        public PrototypeModelTwo(string type)
        {
            this._type = type;
        }
    }
}

And the PrototypeMapper maintains a registry of the created types and their respective model type attribute basing on which the already cached objects are returned or a cloned copy of the requested model type is returned as per the client.


namespace netcore3app.Prototype
{
    public class PrototypeMapper
    {
        private Dictionary<string, IBasePrototype> prototypeRegistry = new Dictionary<string, IBasePrototype>();

        public PrototypeMapper()
        {
            this.AddPrototypes();
        }

        private void AddPrototypes()
        {
            //Add a prototype - PrototypeModelOne
            var modelOne = new PrototypeModelOne("modelone");
            prototypeRegistry.Add("modelone", modelOne);

            var modelTwo = new PrototypeModelTwo("modeltwo");
            prototypeRegistry.Add("modeltwo", modelTwo);
        }

        public IBasePrototype GetPrototypeModel(string modelType)
        {
            IBasePrototype prototype;

            if (prototypeRegistry.TryGetValue(modelType, out prototype))
            {
            }

            return prototype;
        }

        public IBasePrototype GetClonedPrototype(string modelType)
        {
            var prototype = GetPrototypeModel(modelType);
            
            if (prototype != null)
            {
                return prototype.Clone();
            }

            return prototype;
        }
    }
}

And the client makes use of this Mapper instance to communicate for object types required.


namespace netcore3app.Prototype
{
    public class ProtoClient
    {
        public void StartingPoint()
        {
            var protoMapper = new PrototypeMapper();
            
            IBasePrototype proto;
            proto = protoMapper.GetPrototypeModel("modelone");
            Console.WriteLine($"This is a protoype of {proto.Type}");
            proto = protoMapper.GetClonedPrototype("modelone");
            Console.WriteLine($"This is a cloned protoype of {proto.Type}");
        }
    }
}

We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept