Prototype Pattern: The Polymorphism Trap
Why is copying objects harder than it looks? Explore the 'If-Else' nightmare of manual cloning and why the Prototype Pattern is the only way to handle polymorphic copies.
Imagine a client class that has a reference to a Student object:
Student st1 = getStudent(); // This could return a Student or any of its subclasses
Student st2 = // How do we create a perfect copy of st1?Why do we even need a copy? The most common reason is to modify the object for a specific task without affecting the original "source of truth."
Approach 1: Manual Attribute Copying
The most naive approach is to create a new instance in the client and copy every attribute one by one.
Why Manual Copying Fails
- Private Attributes: The client cannot access
st1.namedirectly. Even if getters exist, it's not guaranteed that every internal state variable is exposed. - Implementation Leakage: The client now knows exactly how many attributes a
Studenthas. This leads to Tight Coupling. - Abstraction Violation: Abstraction means hiding internal details. Manual copying forces the client to peek "under the hood."
Approach 2: The Copy Constructor
To solve the private attribute issue, we could use a Copy Constructor:
public class Student {
private String name;
private int age;
// Copy Constructor
public Student(Student other) {
this.name = other.name;
this.age = other.age;
}
}The client calls: Student st2 = new Student(st1);
The Critical Flaw: The Polymorphism Trap
While the copy constructor handles private attributes, it fails miserably when Inheritance is involved. Imagine Student has a child class called IntelligentStudent.
Student st1 = new IntelligentStudent(); // Polymorphism
Student st2 = new Student(st1); st1is actually anIntelligentStudenton the heap.- But the client calls
new Student(st1). - Result:
st2is a plainStudentobject. We have lost all the data of theIntelligentStudent!
The "If-Else" Nightmare (OCP Violation)
To fix this, the client would have to check the actual type at runtime:
Student st2;
if (st1 instanceof IntelligentStudent) {
st2 = new IntelligentStudent((IntelligentStudent) st1);
} else {
st2 = new Student(st1);
}This violates the Open-Closed Principle. Every time you add a new subclass, you must update every client that performs a copy.
The Solution: "I Know Myself Best"
If a client cannot copy an object safely, the class itself should do it. The object knows its own private fields and its own runtime type.
We introduce a method, traditionally called copy() or clone(). This is the heart of the Prototype Pattern.
public class Student {
public Student copy() {
return new Student(this); // Calls the internal copy constructor
}
}
// Client Code (Simple & Polymorphic)
Student st2 = st1.copy(); Java vs. JavaScript: Cloning Realities
1. In Java (Multi-threaded & Structured)
Java has a built-in Cloneable interface, but it is widely considered "broken" because it doesn't provide a clone() method (it's just a marker). The approach we used above--Copy Method + Copy Constructor--is the industry standard.
2. In JavaScript / TypeScript
In the single-threaded world of JS, we often use the Spread Operator for shallow copies:
const st2 = { ...st1 };The Problem: Spread only does a Shallow Copy. If st1 has a nested object (like an Address), both st1 and st2 will point to the same address object.
For deep, polymorphic copies in JS/TS classes, the Prototype Pattern is still the superior choice.
In the next part, we will learn how to manage these prototypes using the Registry Pattern.
Keep reading