LESSON 4.6
Prototype
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
13 min read
Real-world analogy
Imagine a university or corporate office with an official standardized syllabus, legal NDA contract, or budget spreadsheet. You do not retype the entire twenty-page legal document from a blank page every time a new client joins.
Instead, you open the pre-approved template, make a photocopy (or digital duplicate), fill in the specific client names and dates, and save the copy. The master template is the Prototype; cloning it produces an independent document in milliseconds without repeating the original authorship effort.
The problem
Consider a graphic design application or a gaming engine with complex entities (such as game units, visual diagrams, or network node configurations). Instantiating an entity from scratch requires loading 3D meshes, reading texture files from disk, calculating collision boundaries, and parsing metadata configs.
If an army of 500 identical goblin warriors or 20 identical chart shapes are spawned, building each from scratch through constructors is slow and duplicates expensive disk/network I/O.
In the naive code below, creating an enemy requires repeating the full setup sequence:
class GameMonster {
constructor(type, health, attackPower, equipment) {
this.type = type;
this.health = health;
this.attackPower = attackPower;
this.equipment = equipment; // Expensive reference array
console.log(`Loaded heavy 3D assets for: ${type}`);
}
}
// Spawning 3 monsters runs heavy initialization 3 times:
const m1 = new GameMonster("GoblinWarrior", 100, 15, ["Rusty Sword", "Shield"]);
const m2 = new GameMonster("GoblinWarrior", 100, 15, ["Rusty Sword", "Shield"]);
console.log(`Spawned: ${m1.type} & ${m2.type}`);import java.util.ArrayList;
import java.util.List;
public class Example {
static class GameMonster {
String type;
int health;
int attackPower;
List<String> equipment;
public GameMonster(String type
The pain is compounded when the object to be copied is held behind an interface: client code cannot call new ConcreteClass() because it only knows the abstract interface, not the concrete runtime type.
The solution
Intent: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
The participants are:
- Prototype: Declares an interface for cloning itself (typically with a
clone()method). - ConcretePrototype: Implements the
clone()operation by duplicating its state (either shallow or deep). - Client: Creates a new object by asking a prototype to clone itself.
- Prototype Registry (Optional): Stores a dictionary of frequently used prototypes accessible by identifier.
Step-by-step implementation
Step 1: Declare the Prototype interface
Define the contract with a clone() method returning a fresh copy of the same interface type.
class Shape {
clone() { throw new Error("clone() must be implemented"); }
}public class Example {
interface Prototype<T> {
T clone();
}
public static void main(String[] args) {}
}Step 2: Implement Concrete Prototype with copy mechanics
In the concrete prototype, implement the copy operation. For objects containing nested references or collections, ensure mutable structures are deeply copied to prevent shared mutations.
Step 3: Implement the Prototype Registry
Provide an in-memory catalog where application modules can retrieve and clone named presets.
The complete example
Here is a runnable game unit manager demonstrating prototype cloning and registry lookups without repeating expensive initial asset generation.
Archer [HP: 80] Skills: [Quick Shot, Eagle Eye]
Archer [HP: 80] Skills: [Quick Shot, Eagle Eye, Poison Arrow]Variations
1. Shallow vs Deep cloning
- Shallow clone: Copies object fields by value. For object references (arrays, sub-objects), only the pointer is copied. Mutations to nested data in the clone inadvertently modify the original!
- Deep clone: Recursively visits the entire object graph, creating fresh copies of every nested object. Essential when state includes mutable collections or configurations.
2. Modern JavaScript: structuredClone
Node.js 17+ and modern browsers provide the global structuredClone() function, which performs native deep cloning with support for circular references, Dates, Sets, and Maps:
const clone = structuredClone(originalObject);3. Copy constructors vs Cloneable in Java
In Java, java.lang.Cloneable is widely considered a broken language design:
- It is a marker interface that does not even declare the
clone()method. Object.clone()is protected and returns an untypedObject.- It throws a checked
CloneNotSupportedException. - It allocates memory through native magic that completely bypasses constructor execution, risking broken invariants.
Best practice in Java: Use copy constructors (new Monster(original)) or static copy factory methods (Monster.copyOf(original)), which are typed, safe, and run standard constructor validations.
Where you've already used it
- Java Collections Framework:
ArrayList(Collection<? extends E> c): Standard copy constructor deep-copying array element pointers.java.util.Collections.unmodifiableList(List)/List.copyOf(): Creates defensive copies of collections.
- JavaScript Object cloning:
Object.assign({}, source)and{ ...source }: Built-in shallow cloning mechanisms for object literals.
- Operating systems (POSIX):
- The
fork()system call clones the calling process's memory space (using copy-on-write page tables) to spawn a new process prototype.
- The
- Git Version Control:
git branchandgit checkout -bclone the existing commit tree reference to start a new branch line.
Pros and cons
| Strengths | Weaknesses |
|---|---|
| Clones objects without coupling to concrete classes | Deep cloning complex graphs with circular references is difficult |
| Avoids repeated expensive initialization logic | Can hide mutable reference leaks if shallow cloning is used by mistake |
| Replaces complicated creator inheritance hierarchies | In Java, Cloneable is fraught with edge cases and traps |
| Allows dynamic configuration of prototypes at runtime | Cloning objects with open sockets or file handles is non-trivial |
When not to use it
Do not use Prototype if your objects are simple, lightweight value records easily constructed via new User(id, name). Only adopt Prototype when instantiation is computationally expensive, requires cloning complex graph state, or depends on runtime templates.
Related patterns
| Pattern | Relationship to Prototype |
|---|---|
| Abstract Factory | Abstract Factory can store a set of prototypes and clone them instead of subclassing creators. |
| Builder | Builder constructs objects step-by-step; Prototype creates objects in one shot by cloning a template. |
| Memento | Memento is a special case of Prototype used for capturing state snapshots for undo/redo history. |
| Singleton | Polar opposites: Singleton guarantees only one instance; Prototype produces multiple identical instances. |
Prototype: character spawner
Refactor a shallow character spawner into a true Prototype pattern with deep cloning and registry support so spawned entities do not leak mutable state.
Click Run tests to evaluate your solution against the behavior and structural test suite.
Interview questions
Q1: Why should you avoid Java's Cloneable interface in production code?
Cloneable is a marker interface with no methods; Object.clone() is protected and returns Object. It bypasses constructors, making it difficult to enforce final field initialization and class invariants. Copy constructors or static copy methods are vastly safer and idiomatic.
Q2: What is the risk of performing a shallow clone on an object with mutable state?
A shallow clone shares references to internal mutable data structures (like lists or maps). If caller A mutates the cloned list, caller B's original list is silently modified as well, causing subtle, hard-to-debug concurrency and state corruption issues.
Q3: How does Prototype help avoid class explosion compared to Factory Method?
Factory Method requires a new Creator subclass for every concrete Product type. Prototype avoids this inheritance hierarchy entirely by holding pre-configured instances in a registry and copying them on demand.
Q4: How do you handle circular references during deep cloning?
Maintain an identity hash map (Map<Object, Object> or WeakMap) tracking objects already visited. If an object has already been cloned in the current traversal, return the cached clone reference instead of recursing infinitely.
Try it
Identity-preserving Object Graph Cloner LLD
Clone a restricted data graph while preserving cycles and shared references without invoking accessors.