Blog Design Pattern

Mastering the Prototype Design Pattern: A Comprehensive Guide to Creational Design Patterns in Java

1. What Is the Prototype Pattern?

The Prototype Design Pattern is a creational design pattern that serves several important purposes in software development.

The Prototype pattern is generally used when:

  • Creating new objects is complex or resource-intensive.
  • We want to create new objects by cloning an existing prototype.

Let’s dive into the details of this pattern.

2. When to Use Prototype Design Pattern

The Prototype Design Pattern is useful when you need to create new objects by cloning existing objects, rather than creating them from scratch. This pattern is particularly beneficial in the following scenarios:

  1. Object Creation Efficiency:
    • The Prototype pattern allows objects to be created by copying an existing object, known as the prototype.
    • Instead of creating new instances from scratch (which can be expensive), we clone an existing instance.
    • This is particularly useful when the cost of creating a new object is higher than copying an existing one.

  2. Reducing Overhead:
    • Some objects require costly initialization or involve complex setups.
    • By using prototypes, we avoid repeating this setup for each new instance.
    • The prototype acts as a blueprint, and we customize it as needed.

  3. Dynamic Object Creation:
    • The Prototype pattern allows us to create new objects dynamically at runtime.
    • We can clone different variations of an object based on user input or system conditions.

  4. Avoiding Subclass Explosion:
    • In scenarios where we have a large number of subclasses, creating instances of each subclass can become unwieldy.
    • The Prototype pattern allows us to create instances without explicitly referring to the class.
    • We can create new objects by copying existing ones, regardless of their specific class.

3. Prototype Design Pattern Example In Java

Prototype Design Pattern

Here’s a step-by-step example of the Prototype design pattern in Java:

Step 1: Define the Prototype interface.

Java
interface Prototype extends Cloneable {
    public Prototype clone() throws CloneNotSupportedException;
}

Step 2: Create a concrete class that implements the Prototype interface.

Java
class Employee implements Prototype {
    private int id;
    private String name;
    private String department;

    public Employee(int id, String name, String department) {
        this.id = id;
        this.name = name;
        this.department = department;
    }

    @Override
    public Prototype clone() throws CloneNotSupportedException {
     // Here, you can implement more complex logic for deep copying if needed.
        return (Employee) super.clone();
    }

    // Getters and setters
}

Step 3: Create a PrototypeFactory class to store and manage the prototypes.

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

class PrototypeFactory {
    private static Map<String, Prototype> prototypes = new HashMap<>();

    public static Prototype getInstance(String type) throws CloneNotSupportedException {
        Prototype prototype = prototypes.get(type);
        return prototype.clone();
    }

    public static void addPrototype(String type, Prototype prototype) {
        prototypes.put(type, prototype);
    }
}

Step 4: Initialize the prototypes and add them to the PrototypeFactory.

Java
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Employee emp1 = new Employee(1, "John", "Development");
        Employee emp2 = new Employee(2, "Jane", "Marketing");

        PrototypeFactory.addPrototype("DEV", emp1);
        PrototypeFactory.addPrototype("MARKETING", emp2);

        // Create new employees based on prototypes
        Employee clonedEmp1 = (Employee) PrototypeFactory.getInstance("DEV");
        Employee clonedEmp2 = (Employee) PrototypeFactory.getInstance("MARKETING");

        System.out.println("Cloned Employee 1: " + clonedEmp1.getName());
        System.out.println("Cloned Employee 2: " + clonedEmp2.getName());
    }
}

In this example, we first define the Prototype interface with a clone() method. Then, we create a concrete Employee class that implements the Prototype interface and overrides the clone() method.

Next, we create a PrototypeFactory class to manage and store the prototypes. The getInstance() method retrieves a prototype from the factory and creates a new object by cloning it.

In the main() method, we create two Employee objects, emp1 and emp2, and add them to the PrototypeFactory as prototypes with keys “DEV” and “MARKETING”, respectively.

Finally, we create a new Employee objects, clonedEmp1 and clonedEmp2, by cloning the prototypes from the factory using the getInstance() method.

By running this code, you should see the following output:

Java
//Output
Cloned Employee 1: John
Cloned Employee 2: Jane

This example demonstrates how the Prototype Design Pattern allows you to create new objects by cloning existing ones, rather than creating them from scratch.

This can be beneficial when object creation is expensive or when you need to create objects with different configurations based on pre-existing prototypes.

Another Example of Creating Prototype Design Pattern

Step1: Define the clone method within a class that implements the Cloneable interface:

Java
public class Notebook implements Cloneable {
    private int numberOfPages;
    private String type;
    private String size;
    private String coverDesign;
    // Additional properties for customization
    // Constructor
    public Notebook(int numberOfPages, String type, String size) {
        this.numberOfPages = numberOfPages;
        this.type = type;
        this.size = size;
    }
    // Clone method
    @Override
    public Notebook clone() throws CloneNotSupportedException {
        // Here, you can implement more complex logic for deep copying if needed.
        return (Notebook) super.clone();
    }
    // Getters and Setters
    public int getNumberOfPages() {
        return numberOfPages;
    }
    public void setNumberOfPages(int numberOfPages) {
        this.numberOfPages = numberOfPages;
    }
    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
    public String getSize() {
        return size;
    }
    public void setSize(String size) {
        this.size = size;
    }
    public String getCoverDesign() {
        return coverDesign;
    }
    public void setCoverDesign(String coverDesign) {
        this.coverDesign = coverDesign;
    }
    // Method to add content (demonstration of customization post-cloning)
    public void addContent(String content) {
        // Imagine this method adds content to the notebook. Implementation is skipped for brevity.
    }
    // toString() method for easy printing
    @Override
    public String toString() {
        return "Notebook{" +
                "numberOfPages=" + numberOfPages +
                ", type='" + type + '\'' +
                ", size='" + size + '\'' +
                ", coverDesign='" + coverDesign + '\'' +
                '}';
    }
}

Step 2: Store the Prototype

A prototype registry stores the initial configurations:

Java
public class NotebookRegistry {
    private Map<String, Notebook> prototypes = new HashMap<>();
    public NotebookRegistry() {
        // Populate the registry with default prototypes
        Notebook ruledNotebook = new Notebook(120, "ruled", "A4");
        prototypes.put("ruled", ruledNotebook);
    }
    public Notebook getPrototype(String key) throws CloneNotSupportedException {
        return (Notebook) prototypes.get(key).clone();
    }
}

Step 3: The client Retrieves and Clones the Prototype

The client code then looks something like this:

Java
public class NotebookClient {
    public static void main(String[] args) {
        NotebookRegistry registry = new NotebookRegistry();
        try {
            Notebook myNotebook = registry.getPrototype("ruled");
            // Customize the cloned notebook
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

Step 4: Customize the Cloned Object

Each cloned Notebook object can be further customized without affecting the original prototype.

Java
public class NotebookClient {
    public static void main(String[] args) {
        NotebookRegistry registry = new NotebookRegistry();
        try {
            // Clone the prototype Notebook
            Notebook myNotebook1 = registry.getPrototype("ruled");
            
            // Customize the cloned notebook for a specific class
            myNotebook1.setCoverDesign("Mathematics Theme");
            myNotebook1.addContent("Trigonometry notes");
            
            System.out.println("Customized Notebook created with cover: " + 
                               myNotebook1.getCoverDesign());
            // Other customizations can follow
            Notebook myNotebook2 = registry.getPrototype("ruledNotebook");
            myNotebook2.setCoverDesign("Science Theme");
            myNotebook2.addContent("Important science notes.");
            
            System.out.println(myNotebook2);
            
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

3. Advantages of Prototype Pattern

  • Efficient object creation: Cloning existing objects is faster than creating new ones.
  • Dynamic object creation: Create variations of objects at runtime.
  • Avoid subclass explosion: Use prototypes without explicitly referring to their classes.

4. Overcoming Object Creation Challenges with the Prototype Design Pattern

  1. Tight Coupling: Embedding the copy logic within the client can lead to tight coupling between the client and the details of the object’s construction and representation. This means changes in the object’s structure or construction process might necessitate changes in all clients that create copies of the object, leading to a maintenance nightmare.
  2. Duplication of Logic: If multiple clients need to create copies of the object, the logic for copying the object might be duplicated across various parts of the application. This violates the DRY (Don’t Repeat Yourself) principle, making the codebase more difficult to maintain and prone to bugs.
  3. Error-Prone: Implementing copy logic, especially for complex objects, can be error-prone. It’s easy to miss out on copying certain attributes, especially if they are added or changed later on in the object’s lifecycle. Deep copies are particularly challenging as they require a thorough duplication of the object and all objects it refers to, recursively.
  4. Lack of Flexibility: Having the copy logic within the client might limit the flexibility in terms of how objects can be copied. For example, there might be scenarios where a shallow copy is sufficient, whereas, in other cases, a deep copy is necessary. Implementing these variations directly within the client can further complicate the client’s code.
  5. Violation of Single Responsibility Principle (SRP): By embedding the copy logic within the client, you’re assigning it an additional responsibility. According to the SRP, a class should have only one reason to change. Managing the creation of object copies is usually not a client’s primary responsibility, which means the client is doing more than it should.

To mitigate these issues, it’s often recommended to use design patterns that encapsulate the creation logic outside of the client. Two common patterns for this purpose are:

  • Prototype Pattern: This pattern involves creating a new object by copying an existing object, known as the prototype. This pattern is particularly useful when the construction of a new object is more efficient or more convenient through copying. The object itself provides a method to clone it, keeping the copying logic encapsulated within the object and thus reducing the risk of errors.
  • Factory Method Pattern: In cases where the object creation logic is complex and might require more than just copying an existing object, a factory method can encapsulate this logic. The factory method is responsible for creating and returning new instances of an object, potentially using different strategies like copying an existing instance, creating a new instance from scratch, or reusing existing instances from a pool.

5. Using External Libraries

Sometimes, implementing certain functionalities from scratch can be time-consuming and error-prone. This is where external libraries come in handy. Here’s why you might need to use external libraries in your Java projects:

  1. Rich Functionality: External libraries provide ready-made solutions for common tasks. For example:
    • Logging libraries (e.g., Log4j, SLF4J) simplify logging.
    • JSON libraries (e.g., Jackson, Gson) handle JSON serialization and deserialization.
    • Database libraries (e.g., Hibernate, JDBC) manage database interactions.
  2. Performance Optimization: Well-established libraries are often optimized for performance and memory usage. Leveraging them can improve your application’s efficiency.
  3. Security and Stability: Reputable libraries undergo rigorous testing and security audits. Using them reduces the risk of introducing vulnerabilities.
  4. Focus on Business Logic: By relying on external libraries, you can focus more on your application’s core business logic rather than reinventing the wheel.

Remember, external libraries are like tools in your toolbox – they help you build robust and feature-rich applications efficiently! 🛠️🚀

In summary, the Prototype pattern promotes reusability, performance optimization, and dynamic object creation by allowing us to copy existing objects as templates for new ones.

Feel free to discuss in the comment section expand on this blog by adding more examples, discussing specific libraries, or diving deeper into the nuances of the Prototype pattern. Happy coding! 🖌️✨🌟

Ref:

  1. https://sourcemaking.com/design_patterns/prototype

🚀 Boost Your Coding Skills with These Design Pattern Guides! 🔥

  1. 🏭 Unlock the Power of Abstract Factory with Our Comprehensive Guide
  2. 🌉 Bridge the Gap with Our Bridge Pattern Guide for Seamless Design
  3. 🧱 Build Robust Software with Our Builder Pattern Guide
  4. 🎨 Decorate Your Code with Elegance: The Decorator Pattern Explained
  5. 🏛️ Simplify Complex Systems with Our Facade Pattern Guide
  6. 👀 Stay Notified with Our In-Depth Observer Pattern Walkthrough
  7. 🦸 Mastering the Singleton Design Pattern in Java: A Guide for Developers
  8. 📦 Factory Method Pattern: The Ultimate Guide to Creating Objects
  9. 🧩 Composite Pattern: Mastering the Art of Hierarchical Data Structures
  10. 💻 Unveiling the Proxy Pattern: Control Access Like a Pro
  11. 🔑 Flyweight Pattern: Optimizing Memory Usage with Ease
  12. 🧪 Prototype Pattern: Cloning Made Simple and Efficient
  13. 🌳 Adapter Pattern: Bridging the Gap Between Incompatible Interfaces
  14. 🕰️ Iterator Pattern: Traversing Data Structures with Elegance
  15. 💼 Strategy Pattern: Encapsulating Algorithms for Ultimate Flexibility
Avatar

Neelabh

About Author

As Neelabh Singh, I am a Senior Software Engineer with 6.6 years of experience, specializing in Java technologies, Microservices, AWS, Algorithms, and Data Structures. I am also a technology blogger and an active participant in several online coding communities.

You may also like

Blog Design Pattern

Understanding the Builder Design Pattern in Java | Creational Design Patterns | CodeTechSummit

Overview The Builder design pattern is a creational pattern used to construct a complex object step by step. It separates
Blog Tech Toolkit

Base64 Decode

Base64 encoding is a technique used to encode binary data into ASCII characters, making it easier to transmit data over