Low-level designDeep dive8 min
Prototype
To duplicate an object without knowing its class, ask the object to copy itself. Each class implements one copy method that knows its own fields, so the code that wants a copy never has to.
Duplicate the selection
The editor copies each selected object and moves the copy ten tiles to the right.
1 · The situation
Duplicating a selection in a level editor
A small game's level editor lets a designer select some placed objects and duplicate them ten tiles to the right. Objects come in different kinds: a Turret sits at one point with a firing range; a Patrol walks a route of points. Both implement Placeable, and the editor works with that interface.
To duplicate, Editor needs a copy of each object. The first version makes copies itself, in copyOf(), by checking each object's class and calling the matching constructor. Python writes move_by(); the other names are the same in all four languages.
import java.util.ArrayList;
import java.util.List;
record Point(int x, int y) {
Point shift(int dx) {
return new Point(x + dx, y);
}
}
interface Placeable {
void moveBy(int dx);
String describe();
}
class Turret implements Placeable {
private Point at;
private final int range;
Turret(Point at, int range) {
this.at = at;
this.range = range;
}
Point at() { return at; }
int range() { return range; }
public void moveBy(int dx) {
at = at.shift(dx);
}
public String describe() {
return "Turret at " + at.x() + "," + at.y() + ", range " + range;
}
}
class Patrol implements Placeable {
private final List<Point> route;
Patrol(List<Point> route) {
this.route = new ArrayList<>(route);
}
List<Point> route() { return route; }
public void moveBy(int dx) {
route.replaceAll(p -> p.shift(dx));
}
public String describe() {
List<String> stops = new ArrayList<>();
for (Point p : route) stops.add(p.x() + "," + p.y());
return "Patrol " + String.join(" -> ", stops);
}
}
class Editor {
List<Placeable> duplicate(List<Placeable> selection, int dx) {
List<Placeable> copies = new ArrayList<>();
for (Placeable p : selection) {
Placeable copy = copyOf(p);
copy.moveBy(dx);
copies.add(copy);
}
return copies;
}
private Placeable copyOf(Placeable p) {
if (p instanceof Turret t) return new Turret(t.at(), t.range());
if (p instanceof Patrol r) return new Patrol(r.route());
throw new IllegalArgumentException("can't copy " + p.describe());
}
}
public class Main {
public static void main(String[] args) {
List<Placeable> selection = List.of(
new Turret(new Point(2, 3), 5),
new Patrol(List.of(new Point(6, 1), new Point(9, 1))));
List<Placeable> copies = new Editor().duplicate(selection, 10);
for (Placeable p : selection) System.out.println(p.describe());
for (Placeable p : copies) System.out.println(p.describe());
}
}The class diagram of this version, drawn from its code: the editor depends on every kind of object it copies.
Turret at 2,3, range 5 Patrol 6,1 -> 9,1 Turret at 12,3, range 5 Patrol 16,1 -> 19,1
The originals, then the copies, ten tiles to the right.
2 · The change
Every new kind of object reaches into the editor
The designers want spawners next: a point that releases a wave every few seconds. A Spawner class is easy. Duplicating it isn't, because copyOf() names every class it can copy:
class Editor {
List<Placeable> duplicate(List<Placeable> selection, int dx) {
List<Placeable> copies = new ArrayList<>();
for (Placeable p : selection) {
Placeable copy = copyOf(p);
copy.moveBy(dx);
copies.add(copy);
}
return copies;
}
private Placeable copyOf(Placeable p) {
if (p instanceof Turret t) return new Turret(t.at(), t.range());
if (p instanceof Patrol r) return new Patrol(r.route());
throw new IllegalArgumentException("can't copy " + p.describe());
}
}The costs go beyond one more if:
- The editor depends on every kind of object. A new kind edits the editor; a forgotten one fails only when someone duplicates it.
- Each class exposes its insides.
Turrethasat()andrange(), andPatrolhands out its route, only so that the editor can copy them. - The editor decides how deep to copy.
Patrol's constructor happens to copy the route list. If it didn't, moving the copy would move the original's route too, and the editor would never know.
The practice checks flag the type test, and recognize copyOf() as a simple factory: a method that creates one of several classes behind a common type.
3 · The fix
Each object copies itself
The object being copied is the only one that knows all its fields and which of them need copying too. Give that job to it:
Placeable gains copy(), which returns a new, independent object of the same kind. Each class implements it with its own fields: Turret passes its point and range; Patrol passes its route, which its constructor copies.
class Turret implements Placeable {
private Point at;
private final int range;
Turret(Point at, int range) {
this.at = at;
this.range = range;
}
public Placeable copy() {
return new Turret(at, range);
}
public void moveBy(int dx) {
at = at.shift(dx);
}
public String describe() {
return "Turret at " + at.x() + "," + at.y() + ", range " + range;
}
}
class Patrol implements Placeable {
private final List<Point> route;
Patrol(List<Point> route) {
this.route = new ArrayList<>(route);
}
public Placeable copy() {
return new Patrol(route);
}
public void moveBy(int dx) {
route.replaceAll(p -> p.shift(dx));
}
public String describe() {
List<String> stops = new ArrayList<>();
for (Point p : route) stops.add(p.x() + "," + p.y());
return "Patrol " + String.join(" -> ", stops);
}
}Turret at 2,3, range 5 Patrol 6,1 -> 9,1 Turret at 12,3, range 5 Patrol 16,1 -> 19,1
The same originals and copies as before.
A Spawner now implements copy() like the others, and the editor duplicates it without a change.
4 · The pattern
Prototype: copies made by the original
Prototype creates objects by copying an existing instance, the prototype, through a method every kind implements. The client decides when to copy; each class decides how. That works even when the client has only an interface, such as a selection of Placeable objects, and doesn't know which classes are in it.
The roles in the editor's code: Placeable is the prototype interface, and each kind of object copies itself.
Role | In the editor | Its job |
|---|---|---|
Prototype |
| Declares |
Concrete prototype |
| Returns an independent copy of itself |
Client |
| Asks for copies and adjusts them |
The editor asks the original for a copy, then moves the copy; the original's route is untouched.
Prototype is also useful when making an object from scratch is expensive or involved: a preset configured once, then copied and adjusted. A palette of ready-made objects that the designer copies from is a prototype registry.
5 · What it costs
Every class must copy itself correctly
- One more method in every class, and a new field must be added to it too.
- Deep or shallow is a decision. A copy that shares a mutable list with its original isn't independent; a copy that duplicates a large shared resource wastes memory.
- Cycles and shared parts are hard. Objects that refer to each other need care, so that copying one doesn't copy the world or lose the links between them.
Field | Copy it? | Why |
|---|---|---|
An immutable value, such as a point or a string | No, share it | Nothing can change it |
A mutable collection the object owns, such as a route | Yes | Otherwise changes to the copy reach the original |
A shared service or resource, such as a texture | No, share it | It isn't part of the object's state |
If objects are immutable, copying is rarely needed: share the original and create changed versions instead. And if the client knows the concrete class, a copy constructor is simpler.
6 · Getting it right
Decide what deep means, class by class
- Copy what the object owns; share what it uses. The route is owned; a reference to the level's texture atlas isn't.
- Return the interface type, so the client never needs a cast.
- Test independence. Change the copy, and check that the original didn't move.
- Don't rely on a generic deep copy for everything. It copies what it finds, including things meant to be shared.
Each language has its own copying tools, and each has a trap:
Language | The usual shape | The trap |
|---|---|---|
Java | A |
|
C++ | A virtual | Copies through a base class slice off the subclass's part; members held by value copy deeply, raw pointers don't |
JavaScript | A | Spread copies one level; |
Python | A |
|
7 · Look-alikes
Copies, snapshots and prototypes
Several ideas involve copying, and one shares the name without being the pattern.
Idea | What it does |
|---|---|
Prototype | Makes a new object by asking an existing one to copy itself, through an interface |
Copy constructor | Copies an object whose class the caller knows |
Factory Method | Lets a subclass choose which class to create from scratch |
Memento | Saves an object's state to restore later, not to make a second object |
JavaScript prototypes | The language's object linking for inheritance; a different idea that shares the word |
The last row trips people up. In JavaScript every object has a prototype it inherits from; that's how the language does inheritance, and it has nothing to do with copying. The pattern is about making independent copies.
8 · In practice
Presets, palettes and duplicate commands
Editors of every kind use the idea: duplicating a shape in a drawing tool, a slide in a deck, or a row in a spreadsheet, without the duplicate command knowing what it's duplicating. Presets work the same way: a configured object is kept as a template and copied when needed.
None of the practice problems needs to copy objects yet. The checks do flag the symptom this pattern removes: code that tests an object's class to decide how to handle it.
Check yourself
4 questions
Takeaways
Remember this
- Prototype: ask an object to copy itself, so the client never needs to know its class.
- Each class knows its own fields and decides what a correct copy is.
- Copy what the object owns; share immutable values and resources it only uses.
- Generic copying tools are either too shallow or too deep for some fields; know which you're using.
- Immutable objects rarely need copying at all.