Prototype Design Pattern explained in 2 minutes

Practical guide to explain Prototype Design Pattern

ยท

3 min read

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.

References

Did you find this article valuable?

Support Snehasish by becoming a sponsor. Any amount is appreciated!

ย