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 +
'}';
}
}
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)
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 |
No specific interface support,
but lazy initialization and thread safety may be considerations. |