Photo by Sumner Mahaffey on Unsplash
Prototype Design Pattern explained in 2 minutes
Practical guide to explain Prototype Design Pattern
Problem Statement
Functional programming heavily prefers Immutability i.e. objects should not be mutated. But what about the cases when we need to modify a specific field of an object? You need to create a copy of the entire object and update that specific field.
Okay, but how do I copy an existing object? Prototype design pattern to the rescue!
public class Car implements Cloneable {
private final String name;
private final List<Integer> mileage;
public Car(String name, List<Integer> mileage) {
this.name = name;
this.mileage = mileage;
}
// Clone using Copy constructor
public Car(Car existing) {
this.name = existing.name;
// deep copy
this.mileage = List.copyOf(existing.getMileage());
}
// Clone using the default clone method.
@Override
public Car clone() {
try {
// by default provides shallow cloning i.e. only clones the references
return (Car) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
public String getName() {
return name;
}
public List<Integer> getMileage() {
return mileage;
}
}
In the above code, we demonstrated two ways to create a clone of an Object in Java - using a custom copy constructor or by implementing the Cloneable
interface.
Cloneable
is a very special interface in Java - it's a marker interface - it does not have any method. When an Object implements Cloneable, JVM changes the protected()
nature of the clone()
method in Object
class to public
() - without which it throws a CloneNotSupportedException
. By default, it provides a shallow clone i.e. only copies the reference.
Using Cloneable to create clones is highly discouraged because of its complicated nature.
Copy Constructors on the other hand are very intuitive and allow you to exactly control the behaviour. You can tweak the code to easily use shallow or deep copying as and when required.
public class Main {
public static void main(String[] args) {
testPrototype();
}
public static void testPrototype() {
List<Integer> mileage = new ArrayList<>();
mileage.add(10);
mileage.add(20);
Car originalCar = new Car("bentley", mileage);
Car shallowClone = originalCar.clone();
Car deepClone = new Car(originalCar);
List<Integer> clonedMileage = shallowClone.getMileage();
System.out.println(shallowClone.getName() + " " + clonedMileage);
mileage.set(0, 50);
// updating the mileage also updated the clonedMileage because they both
// point to the same object (shallow copy)
System.out.println(clonedMileage.get(0).equals(50));
// updating the mileage didn't affect the deep cloned object
System.out.println(deepClone.getMileage().get(0).equals(10));
}
}
// Output
bentley [10, 20]
true
true
Bonus Section
If you are using Lombok
in your application to create boilerplate Java code, then you can use @With
or @Builder(toBuilder = true)
to create clones.
@With
allows you to create a cloned object by updating only a specific field whereas toBuilder()
allows you to override as many fields as possible. You can chain multiple with()
to update multiple fields in succession but that would create a lot of garbage.
What's the benefit?
- Domain logic to clone the object does not spill out to the outer application.
What's the drawback?
Circular dependency can become tricky to resolve. You would need to exclude a specific field to resolve the conflict.
Need to be aware of the type of cloning being performed - shallow/deep.