DEV Community

Cover image for Prototype design pattern
Pranil Tunga
Pranil Tunga

Posted on

Prototype design pattern

As a result, in our previous blog, we examined the information regarding the factory pattern and why and when it should be used. We'll be looking at the prototype design pattern in this article, so let's dive in and try to understand it.

Overview:

This pattern focuses primarily on copying or cloning existing objects in our memory. so that they can be used more effectively and multiple copies can be used by users to perform multiple operations.

The majority of things in many real-life scenarios aren't made from scratch; instead, they're added to or taken away from an existing prototype or model. computers, as an example. There are probably some situations in programming as well where we need to expand the current object prototype so that it can be altered and used to carry out different tasks. In other words, we are reusing the current designs.

What is a prototype?

In our codebase, a "prototype" is a fully or partially existing type. (In most programming languages, "classes" are regarded as the object's prototype; because C is a procedural language, "structs" are used instead.)

Why the prototype pattern?

Imagine that we already have a prototype that is built using the "factory" pattern, but we want to perform multiple operations, or variations, on that prototype. In order to perform variations, we need multiple copies of the object, which is where the "Prototype Design Pattern" comes in very handy.

Let's look at an example to try to understand why we need this.

public class Employee : ICloneable
{
    public Employee(string name, string[] skills)
    {
        Name = name;
        Skills = skills;
    }

    public string Name { get; set; }
    public string[] Skills { get; set; }

    public object Clone()
    {
        return new Employee(Name, Skills);
    }

    public override string ToString()
    {
        return $"{Name} is good in {string.Join(",", Skills)}";
    }
}

public class CloneObjectsMain
{
    public static void Main(string[] args)
    {
        var johnDoe = new Employee("John Doe", new string[] { "Cooking", "Singing" });

        var johnDummy = (Employee)johnDoe.Clone();
        johnDummy.Name = "John Dummy";
        johnDummy.Skills = new string[] { "Coding" };

        Console.WriteLine(johnDoe);
        Console.WriteLine(johnDummy);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, we have a class called "Employee" that contains the name of an employee and an array called "Skills" that the employee can use to store their skills. You'll see that the Employee class uses the ICloneable interface to copy the object; in our case, this is fine, but if you later decide to make a class for skills and use its array, you'll need to modify the new class to inherit ICloneable and provide an implementation for the Clone() method. Otherwise, the new class will share the same reference and run the risk of changing an existing object. As a result, we can say that ICloneable performs a “shallow copy.” Another drawback is that, if you pay close attention, you’ll notice that the Clone() method returns “object” as its return type, which means that each time you clone, you must explicitly typecast the object to the required type. This can result in a number of errors, or the user may accidentally mismatch the types while explicitly converting since there is no restriction on that.

Let's see how to implement the prototype class so that we can deeply copy our objects in order to solve this issue.

namespace DesignPatterns.PrototypePattern
{
    public abstract class EmployeePrototype
    {
        public abstract Employee DeepCopy();
    }

    public class Employee : EmployeePrototype
    {
        public Employee(string name, string[] skills)
        {
            Name = name;
            Skills = skills;
        }

        public string Name { get; set; }
        public string[] Skills { get; set; }

        public override Employee DeepCopy()
        {
            return new Employee(Name, Skills);
        }

        public override string ToString()
        {
            return $"{Name} is good in {string.Join(",", Skills)}";
        }
    }

    public class PrototypePatternMain
    {
        public static void Main(string[] args)
        {
            var johnDoe = new Employee("John Doe", new string[] { "Cooking", "Singing" });

            var johnDummy = johnDoe.DeepCopy();
            johnDummy.Name = "John Dummy";
            johnDummy.Skills = new string[] { "Coding" };

            Console.WriteLine(johnDoe);
            Console.WriteLine(johnDummy);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Since the "DeepCopy()" abstract method returns "Employee," we don't need to worry about explicit conversion. Currently, we are using the "abstract" class to specify the prototype of the Employee class, which contains the abstract method.

But as you can see, each class must have its own prototype, which is very time-consuming when dealing with multiple classes in real-world situations. Instead, we can use generics in this case, and since we have marked all of the methods as abstract, we can also use abstract classes to create interfaces.

namespace DesignPatterns.PrototypePattern
{
    public interface IPrototype<T>
    {
        T DeepCopy();
    }

    public class Employee : IPrototype<Employee>
    {
        public Employee(string name, string[] skills)
        {
            Name = name;
            Skills = skills;
        }

        public string Name { get; set; }
        public string[] Skills { get; set; }

        public Employee DeepCopy()
        {
            return new Employee(Name, Skills);
        }

        public override string ToString()
        {
            return $"{Name} is good in {string.Join(",", Skills)}";
        }
    }

    public class PrototypePatternMain
    {
        public static void Main(string[] args)
        {
            var johnDoe = new Employee("John Doe", new string[] { "Cooking", "Singing" });

            var johnDummy = johnDoe.DeepCopy();
            johnDummy.Name = "John Dummy";
            johnDummy.Skills = new string[] { "Coding" };

            Console.WriteLine(johnDoe);
            Console.WriteLine(johnDummy);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now that deep cloning or copying of existing objects is possible, we can use the same interface across all existing classes.

However, let's assume that in real-world situations, we deal with a hierarchy of classes that we must copy. In that case, we must be concerned with a number of issues, such as exposing appropriate constructors, requiring that every class inherit the "IPrototype" interface, and ensuring that every class implements the "DeepCopy()" method. These issues may be more difficult to implement and can be time-consuming.

You might now be wondering how we can solve or simplify this, and the best option might be to use "Prototype Inheritance."


What is “prototype inheritance”?

By using the DeepCopy() method, we can easily duplicate an existing prototype. However, if someone tried to inherit the prototype, we would also have to inherit the "IPrototype" interface for that type, where we would have to again provide implementation for its methods. This would also repeat the base class creation, which could be problematic if your hierarchy contains 10 to 20 classes.

Let's use an illustration to better understand:

namespace DesignPatterns.PrototypePattern
{
    public interface IPrototype<T>
    {
        T DeepCopy();
    }

    public class Skill : IPrototype<Skill>
    {
        public string SkillName;

        public Skill(string skillName)
        {
            SkillName = skillName;
        }

        public Skill DeepCopy()
        {
            return new Skill(SkillName);
        }
    }

    public class Employee : IPrototype<Employee>
    {
        public string Name;
        public Skill Skill;

        public Employee(string name, Skill skill)
        {
            Name = name;
            Skill = skill;
        }

        public Employee DeepCopy()
        {
            return new Employee(Name, Skill.DeepCopy());
        }
    }

    public class PartTimeEmployee : Employee, IPrototype<PartTimeEmployee>
    {
        public int DailyWorkingHours;
        public PartTimeEmployee(string name, Skill skill, int dailyWorkingHours)
            : base(name, skill)
        {
            DailyWorkingHours = dailyWorkingHours;
        }

        PartTimeEmployee IPrototype<PartTimeEmployee>.DeepCopy()
        {
            return new PartTimeEmployee(Name, Skill.DeepCopy(), DailyWorkingHours);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we are creating the "PartTimeEmployee" class here based on the "Employee" class that already exists. If you pay attention to the parameters "Name" and "Skill," you'll notice that we are performing the same operation while deep copying multiple times.

return new Employee(Name, Skill.DeepCopy());
Enter fullscreen mode Exit fullscreen mode

And

return new PartTimeEmployee(Name, Skill.DeepCopy(), DailyWorkingHours);
Enter fullscreen mode Exit fullscreen mode

are largely the same, and maintaining the same thing in a larger hierarchy of 10 to 20 classes would be disastrous. This might not be an efficient solution, and it also cannot be scaled.

Let's now look at a solution to this issue.

namespace DesignPatterns.PrototypePattern
{
    public interface IPrototype<T>
        where T : new()
    {
        void CopyInto(T destination);

        T DeepCopy()
        {
            T obj = new T();
            CopyInto(obj);
            return obj;
        }
    }

    public class Skill : IPrototype<Skill>
    {
        public string SkillName;

        public Skill() { }

        public Skill(string skillName)
        {
            SkillName = skillName;
        }

        public void CopyInto(Skill destination)
        {
            destination.SkillName = SkillName;
        }
    }

    public class Employee : IPrototype<Employee>
    {
        public string Name;
        public Skill Skill;

        public Employee() { }

        public Employee(string name, Skill skill)
        {
            Name = name;
            Skill = skill;
        }

        public void CopyInto(Employee destination)
        {
            destination.Name = Name;
            destination.Skill = ((IPrototype<Skill>)Skill).DeepCopy();
        }
    }

    public class PartTimeEmployee : Employee, IPrototype<PartTimeEmployee>
    {
        public int DailyWorkingHours;

        public PartTimeEmployee() { }

        public PartTimeEmployee(string name, Skill skill, int dailyWorkingHours)
            : base(name, skill)
        {
            DailyWorkingHours = dailyWorkingHours;
        }

        public void CopyInto(PartTimeEmployee destination)
        {
            base.CopyInto(destination);
            destination.DailyWorkingHours = DailyWorkingHours;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we are now using it correctly. We are also cloning each class within its boundaries, and in the "CopyTo()" method of the "PartTimeEmployee" class, you can see that we are using the CopyTo() method from the base class. This is the benefit we can obtain from using the generic approach in this case.

Additionally, you can use the extension methods on each class to create the purposeful calls if you want to expose better-looking functions as an API for user convenience.

However, for the purposes of this blog, we'll stick with the approach we just discussed. You can also use recursive generics to optimize the code, just as we did in the Builder Pattern earlier.


Is there any other approach?

This time, we'll talk about how this pattern is used in real-world situations. In real-world situations, the original object is serialized, and while the object is being deserialized, it is copied and its state is maintained. Let's quickly look at an example of how we can accomplish this.

namespace DesignPatterns.PrototypePattern
{
    public static class Extensions
    {
        public static T DeepCopy<T>(this T obj)
        {
            using (var ms = new MemoryStream())
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(ms, obj);
                ms.Position = 0;

                return (T)formatter.Deserialize(ms);
            }
        }
    }

    [Serializable]
    public class Skill
    {
        public string SkillName;

        public Skill(string skillName)
        {
            SkillName = skillName;
        }
    }

    [Serializable]
    public class Employee
    {
        public string Name;
        public Skill Skill;

        public Employee(string name, Skill skill)
        {
            Name = name;
            Skill = skill;
        }

        public override string ToString()
        {
            return $"{Name} is good in {Skill.SkillName}";
        }
    }

    [Serializable]
    public class PartTimeEmployee : Employee
    {
        public int DailyWorkingHours;

        public PartTimeEmployee(string name, Skill skill, int dailyWorkingHours)
            : base(name, skill)
        {
            DailyWorkingHours = dailyWorkingHours;
        }

        public override string ToString()
        {
            return $"{base.ToString()} and is spending {DailyWorkingHours} hours daily to improve.";
        }
    }

    public class MainClass
    {
        public static void Main(string[] args)
        {
            var partTimeEmp = new PartTimeEmployee("John Doe", new Skill("Coding"), 10);
            var copyOfObject = partTimeEmp.DeepCopy();

            //Just to demonstrate that it is not mutating
            //original object (i.e. we are deep copying)
            copyOfObject.Name = "Jack Reacher";
            copyOfObject.DailyWorkingHours = 15;
            copyOfObject.Skill = new Skill("Cooking");

            Console.WriteLine(partTimeEmp);
            Console.WriteLine(copyOfObject);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Output of the above code snippet.

You'll see that we are now using a stream to serialize and deserialize the object, which allows us to create a copy of the object without changing the original. This is because the extension method we are using is generic and can be attached to any type of object. You'll also notice that we've added the [Serializable] attribute to our classes so we can serialize them in streams.

You can also use JsonSerialization, XmlSerialization, or Reflection to create an entirely new copy of an object as additional serialization and deserialization methods.


This concludes our discussion of the current pattern; in our subsequent blog, we will discuss the subsequent pattern under the "Creational" gamma category.

Happy Coding...!!!

Top comments (0)