Usage of prototype design pattern in java

Introduction

One of the creational design patterns is the prototype pattern, which can be used to clone existing objects to create new ones. It allows us to avoid the expense of creating an object from scratch by simply cloning an already existing object and making any modifications if needed.

What is a prototype?

The prototype is used to create a new object as a blueprint.It offers an easy solution for cloning an ordinary object, known as a prototype object, for creating a new one. This approach eliminates the need to explicitly use the new keyword to create an instance and offers a flexible and dynamic manner to create objects totally based on the present one.

Prototype Design Pattern Example

Let's see a practical example to better understand how the prototype pattern works in Java. Imagine we've got a class referred to as "person" with several fields including name, age, and hobbies. Now, suppose we require a tool for the creation and copying of objects, a situation in which we should use the prototype design pattern in Java.

It permits you to easily create new instances of an object by utilising the clone() method. This can be particularly beneficial when you have complex objects that might be very expensive to create from scratch.

Using the prototype pattern, you can avoid the overhead of creating new instances whenever they're needed. You can easily clone an old object and make any necessary alterations. This not only saves time but also promotes in organizing and improving the efficiency of your client code.

Implementing Prototype Pattern in Java

You'll need to define a base or abstract class that implements the Cloneable interface. Then, subclasses can override public clone() method to provide their own implementation.


import java.util.*;
// Prototype class
class Person implements Cloneable {

  private String name;
  private int age;
  private List <String> hobbies;

  public Person(String name, int age, List <String> hobbies) {
    this.name = name;
    this.age = age;
    this.hobbies = hobbies;
  }

  public void setHobbies(List <String> hobbies) {
    this.hobbies = hobbies;
  }

  // Getter methods...
  public String getName() {
    return name;
  }

  public int getAge() {
    return age;
  }

  public List <String> getHobbies() {
    return hobbies;
  }

  @Override
  protected Object clone() throws CloneNotSupportedException {
    return (Person) super.clone();
  }

  @Override
  public String toString() {
    return "Person{" +
      "name='" + name + '\'' +
      ", age=" + age +
      ", hobbies=" + hobbies +
      '}';
  }
}

A Cloneable interface is a marker interface indicating the class which implements this interface supports the cloning. If we override the clone() method without implementing Cloneable then the compiler will throw an exception like below.


java.lang.CloneNotSupportedException: com.example.Person
    at java.base/java.lang.Object.clone(Native Method)
    at com.example.Person.clone(Person.java:38)
    at com.example.PrototypePatternExample.main(PrototypePatternExample.java:15)

Let's create copies of the original Person instance and display the properties. By default, the implementation of the clone() method does shallow clone, which means copying the original object properties into the new object properties. There are two types of copying the pattern provides, deep copy and shallow copy. Based on our case we can use either shallow or deep copy.

Shallow clone 

Here simply the original object properties are copied to the new object. It's mostly used when our class contains more immutable fields.

Deep clone

Create a copy of all properties that are needed by our prototype, most when are class contain mutable or reference of other classes. The subclass may not support cloning. In such a case, the base class must handle the CloneNotSupportedException.



import java.util.*;

public class PrototypePatternExample {
  public static void main(String[] args) {
    // Creating a prototype
    List <String> originalHobbies = Arrays.asList("Reading", "Traveling");
    Person originalPerson = new Person("John", 30, new ArrayList<>(originalHobbies));

    try {

      Person clonePerson = originalPerson.clone();
      System.out.println("Original: " + originalPerson);
      System.out.println("Clone person: " + clonePerson);

      // Shallow clone
      Person shallowClone = originalPerson.shallowClone();
      System.out.println("Original: " + originalPerson);
      System.out.println("Shallow Clone: " + shallowClone);

      // Deep clone
      Person deepClone = originalPerson.deepClone();
      System.out.println("Original: " + originalPerson);
      System.out.println("Deep Clone: " + deepClone);

      originalPerson.getHobbies().add("Sports");

      // Displaying the clones after modifying the original
      System.out.println("Original (after modification): " + originalPerson);
      System.out.println("Shallow Clone (after modification): " + shallowClone);
      System.out.println("Deep Clone (after modification): " + deepClone);

    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }
  }
}

In the above code, we are creating a new object "originalPerson". The "shallowClone" and "deepClone" are the copy of the object. Here we are using the existing object instead of creating a new instances.

Let's see a few more examples in Java like the below UML diagram.




public abstract class Employee implements Cloneable {

  private long id;
  private String name;
  private Address address;

  public Employee(long id, String name, Address address) {
    this.id = id;
    this.name = name;
    this.address = address;
  }

  public long getId() {
    return id;
  }

  public String getName() {
    return name;
  }

  public Address getAddress() {
    return address;
  }

  @Override
  protected Employee clone() throws CloneNotSupportedException {
    return (Employee) super.clone();
  }

  @Override
  public String toString() {
    return "id= " + id + ", name= " + name + ", address= " + address;
  }

}


public class Address {

  private String streetName;
  private int postalCode;

  public Address(String streetName, int postalCode) {
    this.streetName = streetName;
    this.postalCode = postalCode;
  }

  public void setStreetName(String streetName) {
    this.streetName = streetName;
  }

  public void setPostalCode(int postalCode) {
    this.postalCode = postalCode;
  }

  public String getStreetName() {
    return streetName;
  }

  public int getPostalCode() {
    return postalCode;
  }

  @Override
  public String toString() {
    return "streetName= " + streetName + ", postalCode= " + postalCode;
  }

}


public class FullTimeEmployee extends Employee {

  private int salary;
  private int bonus;

  public FullTimeEmployee(long id, String name, Address address, int salary, int bonus) {
    super(id, name, address);
    this.salary = salary;
    this.bonus = bonus;
  }

  public int getSalary() {
    return salary;
  }

  public int getBonus() {
    return bonus;
  }

  @Override
  protected FullTimeEmployee clone() throws CloneNotSupportedException {
    return (FullTimeEmployee) super.clone();
  }

  @Override
  public String toString() {
    return "{" + super.toString() + ",salary=" + salary + ", bonus=" + bonus + "}";
  }
} 
    


public class PartTimeEmployee extends Employee {

  private float payPerHour;
  private String contractPeriod;

  public PartTimeEmployee(long id, String name, Address address, float payPerHour, String contractPeriod) {
    super(id, name, address);
    this.payPerHour = payPerHour;
    this.contractPeriod = contractPeriod;
  }

  public float getPayPerHour() {
    return payPerHour;
  }

  public String getContractPeriod() {
    return contractPeriod;
  }

  @Override
  protected PartTimeEmployee clone() throws CloneNotSupportedException {
    return (PartTimeEmployee) super.clone();
  }

  @Override
  public String toString() {
    return "{" + super.toString() + ",payPerHour=" + payPerHour + ", contractPeriod=" + contractPeriod + "}";
  }

}  
     

Creating Prototype Registry

A prototype registry is a centralised prototype registry for managing and storing prototype instances. Clients can use this registry to access and clone prototypes without having to manually instantiate them.Below we used Map to store the instances, allowing the client to add or retrieve objects by key. In the constructor, we added employee objects to the registry map.

It's useful when you have large class properties that do not change state between the instances and also helpful when we are using composite and decorator patterns.

It's useful when you have large class properties that do not change state between the instances and also helpful when we are using composite and decorator patterns


import java.util.HashMap;
import java.util.Map;

class PrototypeRegistry {

  private Map<String,Employee> prototypes;

  public PrototypeRegistry() {
    prototypes = new HashMap<>();
    // Adding prototypes to the registry
    prototypes.put("fullTimeEmployee", new FullTimeEmployee(12, "Pater", new Address("Stoneleigh Place", 600024),
      1000, 100));
    prototypes.put("partTimeEmployee", new PartTimeEmployee(13, "John", new Address("Buckfast Street", 600033), 20,
      "6M"));
  }

  public void addEmployee(String key, Employee prototype) {
    prototypes.put(key, prototype);
  }

  public Employee getEmployee(String key) throws CloneNotSupportedException {
    return prototypes.get(key).clone();
  }
  
}
    

Prototype Pattern vs Singleton Design Pattern

When comparing the Prototype Pattern to other design patterns, it is important to consider its advantages and use cases in comparison to other pattern like Singleton Design Pattern.

Prototype

Singleton

pattern is used to make new objects by copying an old object, known as the prototype.

Ensures that a class has only one instance and provides a global point of access to it.

Multiple instances can be created from the same prototype.

Only one instance is created and shared globally.

Useful when object creation is more complex or resource-intensive than copying an existing instance.

Suitable when exactly one instance of a class is required to control actions or resources.

Supports the Cloneable interface for enabling cloning.

No specific interface support, but lazy initialization and thread safety may be considerations.

Post a Comment

Previous Post Next Post

Recent Posts

Facebook