Prototype Pattern: Creating Objects with Cloning

The Prototype pattern is a creational pattern that allows developers to create new objects by cloning existing ones, rather than creating them from scratch. The pattern involves creating a prototype object that serves as a template for creating new objects with similar characteristics. These new objects are then created by cloning the prototype and modifying its properties as necessary.

The Prototype pattern is useful in situations where creating new objects from scratch can be time-consuming or resource-intensive. By using a prototype, developers can save time and resources by reusing existing objects and modifying them as needed.

In addition to its practical benefits, the Prototype pattern also promotes modularity and flexibility in design. By separating the creation of objects from their implementation, developers can more easily modify and extend their code without affecting the overall structure of the system.

When to use prototype design pattern

Here are some scenarios where the Prototype pattern can be particularly useful:

  1. When creating new objects requires complex initialization logic or extensive computation. By using a prototype, you can avoid repeating this logic each time a new object is created.
  2. When you need to create many objects with similar characteristics, but with slight variations. By using a prototype, you can easily modify the properties of the cloned objects as needed.
  3. When you want to isolate the creation of objects from their implementation. The Prototype pattern allows you to create objects without knowing their concrete classes, promoting modularity and flexibility in design.
  4. When you need to create objects dynamically at runtime based on user input or other factors. The Prototype pattern allows you to create new objects on the fly by cloning an existing prototype.

When to NOT use prototype pattern

  1. When creating new objects from scratch is not time-consuming or resource-intensive. If you only need to create a few objects, or if the initialization logic is relatively simple, then the benefits of using a prototype may not outweigh the added complexity.
  2. When the objects being created have complex dependencies or are closely tied to their environment. In these situations, it may be more appropriate to use a different creational pattern, such as the Abstract Factory pattern or the Builder pattern.
  3. When the objects being created require deep copying or customization. If the objects being created require significant modifications beyond simple property changes, then using a prototype may not be the most efficient or effective approach.
  4. When the objects being created are not easily cloneable or serializable. If the objects being created have complex or non-standard internal structures, then it may be difficult or impossible to create an accurate clone using the Prototype pattern.

Usecases

  1. java.lang.Object#clone(): the clone() method creates a new object that is a copy of the original object. This method is implemented using the Prototype pattern.
  2. Serialization and deserialization: When an object is serialized, its state is written to a byte stream. When the byte stream is deserialized, a new object is created that has the same state as the original object. This process is similar to the Prototype pattern, where a new object is created by copying the state of an existing object.
  3. ThreadLocal: The ThreadLocal class allows you to create a separate copy of an object for each thread that accesses it. This is achieved by creating a new copy of the object for each thread using the Prototype pattern.
  4. Copy constructors: Copy constructors are constructors that take an object of the same type as their argument and create a new object that is a copy of the original object. This is another way of implementing the Prototype pattern.
  5. Flyweight pattern: The Flyweight pattern is used to reduce memory usage by sharing objects that have the same state. This pattern can be implemented using the Prototype pattern by creating a pool of prototype objects and creating new objects by cloning the prototypes.

Implementation

// Person.java
@Getter
@Setter
public class Person implements Cloneable {
    private String name;
    private int age;
    private Address address;

    public Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    @Override
    public Person clone() {
        try {
            return (Person) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

// Address.java
@Getter
@Setter
@AllArgsConstructor
public class Address {
    private String street;
    private String city;
    private String state;
    private String zip;
}

// Example usage:
Address address = new Address("123 Main St", "Anytown", "USA", "12345");
Person originalPerson = new Person("Alice", 30, address);
Person clonedPerson = originalPerson.clone();
Java

Above snippet uses Lombok’s @Getter, @Setter & @AllArgsConstructor annotations to remove boilerplate code.

Now, In above example, we define a Person class that implements the Cloneable interface, which allows the Object#clone() method to be called on instances of the class. We also define an Address class that is used as a non-primitive field in the Person class.

To create a clone of the original Person object, we simply call the clone() method on the original object. This creates and returns a new object that is a copy of the original object, with all of its fields copied by reference. We can then modify the fields of the cloned object as needed without affecting the original object.

Unit Tests

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class PrototypePatternTest {
    @Test
    public void testPrototypePattern() {
        Address address = new Address("123 Main St", "Anytown", "USA", "12345");
        Person originalPerson = new Person("Alice", 30, address);

        // Test that the cloned object is not the same as the original object
        Person clonedPerson = originalPerson.clone();
        assertNotSame(originalPerson, clonedPerson);

        // Test that the cloned object has the same fields as the original object
        assertEquals(originalPerson.getName(), clonedPerson.getName());
        assertEquals(originalPerson.getAge(), clonedPerson.getAge());
        assertSame(originalPerson.getAddress(), clonedPerson.getAddress());

        // Test that modifying the cloned object does not affect the original object
        clonedPerson.setName("Bob");
        clonedPerson.setAge(40);
        clonedPerson.getAddress().setStreet("456 Oak St");
        clonedPerson.getAddress().setCity("Othertown");
        clonedPerson.getAddress().setState("Canada");
        clonedPerson.getAddress().setZip("67890");

        assertEquals("Alice", originalPerson.getName());
        assertEquals(30, originalPerson.getAge());
        assertEquals("123 Main St", originalPerson.getAddress().getStreet());
        assertEquals("Anytown", originalPerson.getAddress().getCity());
        assertEquals("USA", originalPerson.getAddress().getState());
        assertEquals("12345", originalPerson.getAddress().getZip());
    }
}
Java

Anti-Patterns

While the Prototype pattern can be useful in many situations, there are also some anti-patterns that can arise when using this pattern.

  • Overuse of the clone() method: The clone() method can be very useful, but overuse of this method can lead to code that is difficult to maintain and understand. In particular, if the object being cloned contains complex or mutable state, it can be difficult to ensure that the cloned object is an accurate copy of the original object.
  • Lack of encapsulation: The Prototype pattern can be used to create objects that are similar to an existing object, but with some differences in their state. However, if the state of the original object is exposed or not properly encapsulated, it can be difficult to ensure that the cloned objects are properly initialized.
  • Coupling between prototype objects: If the prototype objects used in the Prototype pattern are tightly coupled to other parts of the system, it can be difficult to modify or replace these objects without affecting the rest of the system. This can lead to code that is inflexible and difficult to maintain.

Incorrect Implementation of Prototype Pattern

  • Not implementing the Cloneable interface: The Prototype pattern requires the use of the clone() method to create copies of objects. However, the clone() method is protected, and can only be called from within the class or from a subclass. To properly use the clone() method, the object being cloned must implement the Cloneable interface. If this interface is not implemented, a CloneNotSupportedException will be thrown.
  • Incorrect implementation of the clone() method: The clone() method creates a new instance of an object that is a copy of the original object. However, this copy is a shallow copy, which means that the fields in the new instance are references to the same objects as the original instance. If the original instance has mutable fields, changes to those fields will be reflected in all copies of the instance. To properly implement the Prototype pattern, the clone() method should be overridden to create a deep copy of the object.
  • Using the new operator to create prototype objects: The whole point of the Prototype pattern is to avoid using the new operator to create new objects. If the new operator is used to create prototype objects, there is no benefit to using the Prototype pattern.
  • Incomplete object initialization: When using the Prototype pattern, the cloned object should be initialized with the same values as the original object. If this initialization is not done correctly, the cloned object may not behave as expected.