Make a new object by copying an existing one. The pattern is a few lines. What matters in practice is what the copy still shares with the original.
Open a food-delivery app and tap Reorder on last Tuesday's dinner: the same pizza, the same two colas, the same note for the rider. The app makes a new cart from the old one. Then you raise the colas to three, and the cart on screen says 420. The question that decides whether this feature is correct is a small one: when the app copied the cart, did it copy the items, or did the new cart end up pointing at the old order's items?
The same question comes up when you duplicate a calendar event, take a snapshot or hand out a template. The Prototype pattern is making a new object by copying an existing one, and where copy is one call the pattern is trivial. The part that is not trivial is shallow versus deep: which parts the copy owns, and which it merely points at. This lesson shows the bug first, then the fix in Python, Java, C++ and JavaScript, then a design where the question mostly disappears, and then Prototype as the book means it: a registry of templates you clone. No case study uses Prototype by name; the text editor takes snapshots, which are copies, and the meeting scheduler makes a series of meetings from one booking.
A photocopy of a sheet that says see Asha's card still points at the same card. A deep copy writes the numbers out on the new sheet.
The first version: reorder as a shallow copy#
The cart holds a list of items, and each item has a name, a quantity and a price. "Reorder" copies the cart with each language's usual one-liner, then the customer changes one quantity:
# shallow_v1.py: "reorder" as a shallow copy shares the Item objects with the past order
import copy
class Item:
def __init__(self, name: str, qty: int, price: int) -> None:
self.name, self.qty, self.price = name, qty, price
class Cart:
def __init__(self, items, note: str) -> None:
self.items, self.note = items, note
def total(self) -> int:
return sum(i.qty * i.price for i in self.items)
def describe(self) -> str:
return ", ".join(f"{i.name} x{i.qty}" for i in self.items) + f" = {self.total()}"
past = Cart([Item("pizza", 1, 300), Item("cola", 2, 40)], "ring the bell")
print("past order:", past.describe())
reorder = copy.copy(past) # a new Cart, but the same list and the same Item objects
reorder.note = "leave at gate" # replacing a string on the copy is safe: the past order keeps its own
reorder.items[1].qty = 3 # but this changes an Item that both carts hold
print("reorder, cola raised to 3:", reorder.describe())
print("past order now:", past.describe())
print(f'notes: past "{past.note}", reorder "{reorder.note}"')
print("shared item objects:", sum(a is b for a, b in zip(past.items, reorder.items)), "of", len(past.items))// ShallowV1.java: "reorder" as a shallow clone() shares the Item objects with the past order
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
class ShallowV1 {
static final class Item {
final String name;
int qty;
final int price;
Item(String name, int qty, int price) {
this.name = name;
this.qty = qty;
this.price = price;
}
}
static final class Cart implements Cloneable {
List<Item> items;
String note;
Cart(List<Item> items, String note) {
this.items = items;
this.note = note;
}
int total() { return items.stream().mapToInt(i -> i.qty * i.price).sum(); }
String describe() {
return items.stream().map(i -> i.name + " x" + i.qty).collect(Collectors.joining(", ")) + " = " + total();
}
@Override
public Cart clone() {
try {
return (Cart) super.clone(); // a field-by-field copy: items points at the same list
} catch (CloneNotSupportedException e) {
throw new AssertionError(e); // cannot happen: Cart implements Cloneable
}
}
}
public static void main(String[] args) {
Cart past = new Cart(new ArrayList<>(List.of(new Item("pizza", 1, 300), new Item("cola", 2, 40))), "ring the bell");
System.out.println("past order: " + past.describe());
Cart reorder = past.clone(); // a new Cart, but the same list and the same Item objects
reorder.note = "leave at gate"; // replacing a string on the copy is safe: the past order keeps its own
reorder.items.get(1).qty = 3; // but this changes an Item that both carts hold
System.out.println("reorder, cola raised to 3: " + reorder.describe());
System.out.println("past order now: " + past.describe());
System.out.println("notes: past \"" + past.note + "\", reorder \"" + reorder.note + "\"");
int shared = 0;
for (int i = 0; i < past.items.size(); i++) if (past.items.get(i) == reorder.items.get(i)) shared++;
System.out.println("shared item objects: " + shared + " of " + past.items.size());
}
}// shallow_v1.cpp: "reorder" with the compiler's copy constructor shares the Items, because the cart holds pointers
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Item {
std::string name;
int qty;
int price;
};
struct Cart {
std::vector<std::shared_ptr<Item>> items; // pointers: copying the vector copies the pointers, not the Items
std::string note;
int total() const {
int sum = 0;
for (const auto& i : items) sum += i->qty * i->price;
return sum;
}
std::string describe() const {
std::string text;
for (std::size_t k = 0; k < items.size(); ++k) text += (k ? ", " : "") + items[k]->name + " x" + std::to_string(items[k]->qty);
return text + " = " + std::to_string(total());
}
};
int main() {
Cart past;
past.items = {std::make_shared<Item>(Item{"pizza", 1, 300}), std::make_shared<Item>(Item{"cola", 2, 40})};
past.note = "ring the bell";
std::cout << "past order: " << past.describe() << "\n";
Cart reorder = past; // the compiler's copy constructor copies member by member: the pointers are copied, so the Items are shared
reorder.note = "leave at gate"; // replacing a string on the copy is safe: the past order keeps its own
reorder.items[1]->qty = 3; // but this changes an Item that both carts hold
std::cout << "reorder, cola raised to 3: " << reorder.describe() << "\n";
std::cout << "past order now: " << past.describe() << "\n";
std::cout << "notes: past \"" << past.note << "\", reorder \"" << reorder.note << "\"\n";
int shared = 0;
for (std::size_t i = 0; i < past.items.size(); ++i) shared += past.items[i] == reorder.items[i] ? 1 : 0;
std::cout << "shared item objects: " << shared << " of " << past.items.size() << "\n";
}// shallowV1.js: "reorder" with the spread operator shares the item objects with the past order
const total = (cart) => cart.items.reduce((sum, i) => sum + i.qty * i.price, 0);
const describe = (cart) => cart.items.map((i) => `${i.name} x${i.qty}`).join(", ") + ` = ${total(cart)}`;
const past = { items: [{ name: "pizza", qty: 1, price: 300 }, { name: "cola", qty: 2, price: 40 }], note: "ring the bell" };
console.log("past order:", describe(past));
const reorder = { ...past }; // a new object, but the same items array and the same item objects
reorder.note = "leave at gate"; // replacing a string on the copy is safe: the past order keeps its own
reorder.items[1].qty = 3; // but this changes an item that both carts hold
console.log("reorder, cola raised to 3:", describe(reorder));
console.log("past order now:", describe(past));
console.log(`notes: past "${past.note}", reorder "${reorder.note}"`);
console.log("shared item objects:", past.items.filter((item, k) => item === reorder.items[k]).length, "of", past.items.length);past order: pizza x1, cola x2 = 380
reorder, cola raised to 3: pizza x1, cola x3 = 420
past order now: pizza x1, cola x3 = 420
notes: past "ring the bell", reorder "leave at gate"
shared item objects: 2 of 2The numbers: the past order is 300 for the pizza plus 2 colas at 40, which is 380. With the colas raised to 3 the reorder should be 300 plus 120, which is 420, and it is. But the past order now says 420 as well: last Tuesday's receipt changed. The note shows the other half of the lesson. Replacing the note on the copy did not touch the past order's note, because the copy just points its own field at a new string. What went wrong was changing something inside an item, and the last line says why: 2 of 2 item objects are the same objects in both carts. The copy duplicated the cart, but not what the cart holds.
- The past changes. A reorder, snapshot or template that shares items with its source lets edits on one show up in the other.
- Every language hides it.
copy.copy,clone(), a default C++ copy constructor and the spread operator all look like "make a copy", and all share the nested parts. - Later fields change the answer. A cart of strings and numbers copies safely. The day someone adds a list, the same one-liner starts sharing.
Why did the past order change?
The reorder replaced its note and the past order kept its own note. The reorder changed one item's quantity and the past order changed too. What is the difference between the two edits?
One strong answer
One edit replaced a field of the copy, and the other changed an object the copy shares. Assigning reorder.note = "..." makes the copy's note field point at a new string and leaves the original's field alone. reorder.items[1].qty = 3 follows the shared list to the shared item and changes it in place, so both carts see it. A shallow copy duplicates the outer object and copies its references, so the second kind of edit is visible from both sides.
The pattern#
The Design Patterns book (1994, Gamma, Helm, Johnson and Vlissides) states the intent of Prototype as: "Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype."
The idea is to keep a ready-made object, the prototype, and make new ones by copying it. The book's structure has a clone() operation on the prototype and a client that asks for a copy, often through a registry of named prototypes. What makes it a pattern, and not just a copy, is that clone() is called through the interface: the client copies an object without knowing its concrete class. A copy constructor cannot do that, because new Cart(other) names the class. Which parts the copy shares is left to the implementer, and that is where the bugs are.
| Role | What it does in the code |
|---|---|
| Prototype | The interface with clone(): here Event.clone() |
| Concrete prototype | A class that knows how to copy itself, including which of its parts must be copied and which can be shared |
| Client | Asks for a copy instead of constructing: registry.create("standup", when) |
| Prototype registry (the book calls it a prototype manager) | Keeps named templates and hands out copies of them, never the template itself |
%% title: Prototype: a registry hands out copies of templates
classDiagram
class Prototype {
<<interface>>
+clone() Prototype
}
class Event {
+title str
+when str
+attendees list
+reminders list
+clone() Event
+describe() str
}
class TemplateRegistry {
-templates dict
+register(name, event)
+create(name, when) Event
}
class Client
Prototype <|.. Event
TemplateRegistry o-- Event : keeps templates
Client ..> TemplateRegistry : asks for a copy
TemplateRegistry ..> Event : clonesRead the diagram. The client never touches a template: it asks the registry for create(name, when), which calls clone() on the stored Event and sets what differs. The template is long-lived and shared, so handing it out directly would be a bug.
Now the shallow-versus-deep picture, which is what clone() has to get right:
%% title: Shallow copy: two carts, one list, the same items flowchart LR P["past cart"] --> L["items list"] R["reorder cart"] --> L L --> A["pizza x1"] L --> B["cola x2"]
%% title: Deep copy: each cart owns its list and its items flowchart LR P["past cart"] --> L1["items list"] R["reorder cart"] --> L2["items list, copied"] L1 --> A1["pizza x1"] L1 --> B1["cola x2"] L2 --> A2["pizza x1, copied"] L2 --> B2["cola x2, copied"]
In the first picture both carts point at one list, so changing the cola changes it for both. In the second, the copy owns a new list and new items. A third option is to make the items immutable, so that sharing them is safe.
A shallow copy of a cart gives two carts and one items list, so there is only one cola. Changing it through either cart changes it for both.
Shallow and deep copy, in each language#
The same two ideas have different spellings in each language, and different traps:
| Shallow copy | Deep copy | What to know | |
|---|---|---|---|
| Python | copy.copy(x), x.copy() on a list or dict, list(x), x[:] |
copy.deepcopy(x) |
deepcopy follows references recursively with a memo, so cycles and shared parts are copied once. It copies everything reachable, including a collaborator the object only uses, and raises TypeError on things it cannot copy, such as a lock. Classes can control it with __copy__ and __deepcopy__ |
| Java | Object.clone() (through Cloneable), new ArrayList<>(other) |
A copy constructor or a copy factory that copies each part, or a clone() you write to do so |
clone() is a weak design: Cloneable is a marker with no clone method, Object.clone() is protected and throws the checked CloneNotSupportedException (so every override needs the try/catch the first version has), it copies field by field without calling a constructor, it is shallow, and it cannot reassign a final field. Effective Java recommends a copy constructor or copy factory. An array's clone() is fine but shallow for object elements |
| C++ | The compiler's copy constructor and assignment: member by member | The same, when the members are values. Write your own when members are pointers | A std::vector<Item> or std::string member copies its contents, so the default is already a deep copy. A raw pointer or shared_ptr member copies the pointer, so it is shallow. A unique_ptr member makes the class non-copyable until you write the copy |
| JavaScript | { ...x }, [...a], Object.assign({}, x) |
structuredClone(x) |
Spread copies own enumerable properties only, so a class instance becomes a plain object without its prototype's methods. structuredClone (Node 17+ and browsers) is deep and handles cycles, Date, Map and Set, but also returns plain objects (no prototype, no #private fields), and throws DataCloneError on functions. The JSON.parse(JSON.stringify(x)) trick turns a Date into a string and fails on cycles |
The code: a deep copy#
The fix is to copy the things the cart owns. Here is the same reorder with a deep copy in each language: copy.deepcopy, a Java copy constructor, a C++ copy constructor, and structuredClone:
# deep_copy.py: "reorder" as a deep copy: the new cart owns new Item objects
import copy
class Item:
def __init__(self, name: str, qty: int, price: int) -> None:
self.name, self.qty, self.price = name, qty, price
class Cart:
def __init__(self, items, note: str) -> None:
self.items, self.note = items, note
def total(self) -> int:
return sum(i.qty * i.price for i in self.items)
def describe(self) -> str:
return ", ".join(f"{i.name} x{i.qty}" for i in self.items) + f" = {self.total()}"
past = Cart([Item("pizza", 1, 300), Item("cola", 2, 40)], "ring the bell")
print("past order:", past.describe())
reorder = copy.deepcopy(past) # copies the list and every Item in it
reorder.note = "leave at gate"
reorder.items[1].qty = 3
print("reorder, cola raised to 3:", reorder.describe())
print("past order now:", past.describe())
print(f'notes: past "{past.note}", reorder "{reorder.note}"')
print("shared item objects:", sum(a is b for a, b in zip(past.items, reorder.items)), "of", len(past.items))// DeepCopy.java: "reorder" with copy constructors: the new cart owns new Item objects
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
class DeepCopy {
static final class Item {
final String name;
int qty;
final int price;
Item(String name, int qty, int price) {
this.name = name;
this.qty = qty;
this.price = price;
}
Item(Item other) { this(other.name, other.qty, other.price); } // a copy constructor
}
static final class Cart {
final List<Item> items;
String note;
Cart(List<Item> items, String note) {
this.items = items;
this.note = note;
}
Cart(Cart other) { // a copy constructor: copy the list, and copy each Item in it
this.items = new ArrayList<>();
for (Item i : other.items) this.items.add(new Item(i));
this.note = other.note;
}
int total() { return items.stream().mapToInt(i -> i.qty * i.price).sum(); }
String describe() {
return items.stream().map(i -> i.name + " x" + i.qty).collect(Collectors.joining(", ")) + " = " + total();
}
}
public static void main(String[] args) {
Cart past = new Cart(new ArrayList<>(List.of(new Item("pizza", 1, 300), new Item("cola", 2, 40))), "ring the bell");
System.out.println("past order: " + past.describe());
Cart reorder = new Cart(past); // copies the list and every Item in it
reorder.note = "leave at gate";
reorder.items.get(1).qty = 3;
System.out.println("reorder, cola raised to 3: " + reorder.describe());
System.out.println("past order now: " + past.describe());
System.out.println("notes: past \"" + past.note + "\", reorder \"" + reorder.note + "\"");
int shared = 0;
for (int i = 0; i < past.items.size(); i++) if (past.items.get(i) == reorder.items.get(i)) shared++;
System.out.println("shared item objects: " + shared + " of " + past.items.size());
}
}// deep_copy.cpp: "reorder" with a hand-written copy constructor: the new cart owns new Items (rule of five)
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
struct Item {
std::string name;
int qty;
int price;
};
class Cart {
public:
std::vector<std::shared_ptr<Item>> items;
std::string note;
Cart() = default;
Cart(const Cart& other) : note(other.note) { // copy constructor: copy every Item, not just the pointer
for (const auto& i : other.items) items.push_back(std::make_shared<Item>(*i));
}
Cart& operator=(const Cart& other) { // copy assignment: copy first, then swap, so a failure leaves *this intact
Cart copy(other);
items.swap(copy.items);
note.swap(copy.note);
return *this;
}
Cart(Cart&&) noexcept = default; // the other three of the five: moving steals, it does not copy
Cart& operator=(Cart&&) noexcept = default;
~Cart() = default;
int total() const {
int sum = 0;
for (const auto& i : items) sum += i->qty * i->price;
return sum;
}
std::string describe() const {
std::string text;
for (std::size_t k = 0; k < items.size(); ++k) text += (k ? ", " : "") + items[k]->name + " x" + std::to_string(items[k]->qty);
return text + " = " + std::to_string(total());
}
};
int main() {
Cart past;
past.items = {std::make_shared<Item>(Item{"pizza", 1, 300}), std::make_shared<Item>(Item{"cola", 2, 40})};
past.note = "ring the bell";
std::cout << "past order: " << past.describe() << "\n";
Cart reorder = past; // now calls the copy constructor above, which copies every Item
reorder.note = "leave at gate";
reorder.items[1]->qty = 3;
std::cout << "reorder, cola raised to 3: " << reorder.describe() << "\n";
std::cout << "past order now: " << past.describe() << "\n";
std::cout << "notes: past \"" << past.note << "\", reorder \"" << reorder.note << "\"\n";
int shared = 0;
for (std::size_t i = 0; i < past.items.size(); ++i) shared += past.items[i] == reorder.items[i] ? 1 : 0;
std::cout << "shared item objects: " << shared << " of " << past.items.size() << "\n";
}// deepCopy.js: "reorder" with structuredClone: the new cart owns new item objects
const total = (cart) => cart.items.reduce((sum, i) => sum + i.qty * i.price, 0);
const describe = (cart) => cart.items.map((i) => `${i.name} x${i.qty}`).join(", ") + ` = ${total(cart)}`;
const past = { items: [{ name: "pizza", qty: 1, price: 300 }, { name: "cola", qty: 2, price: 40 }], note: "ring the bell" };
console.log("past order:", describe(past));
const reorder = structuredClone(past); // copies the array and every item in it
reorder.note = "leave at gate";
reorder.items[1].qty = 3;
console.log("reorder, cola raised to 3:", describe(reorder));
console.log("past order now:", describe(past));
console.log(`notes: past "${past.note}", reorder "${reorder.note}"`);
console.log("shared item objects:", past.items.filter((item, k) => item === reorder.items[k]).length, "of", past.items.length);past order: pizza x1, cola x2 = 380
reorder, cola raised to 3: pizza x1, cola x3 = 420
past order now: pizza x1, cola x2 = 380
notes: past "ring the bell", reorder "leave at gate"
shared item objects: 0 of 2Now the reorder says 420 and the past order still says 380, because 0 of 2 item objects are shared. Each language needed a different tool, with a limit worth knowing:
- Python's
deepcopycopies everything it can reach. If the cart also held the app's shared price list,deepcopywould duplicate that too. A class can copy what it owns and share what it only uses by defining__deepcopy__, which receives thememodictionary. - Java's
clone()is why the lesson shows a copy constructor. The default is shallow, skips constructors, makes every caller handle a checked exception and cannot rebuild afinallist field. A copy constructor or a static copy factory is plain code and works withfinal. - C++'s default copy is memberwise, so deep or shallow depends on the member types. The program above holds
shared_ptr<Item>, which copies as a pointer, so it had to write the copy by hand. That raises the rule of three (if a class needs a custom destructor, copy constructor or copy assignment, it probably needs all three) and, since C++11 added moving, the rule of five (the same with the move constructor and move assignment). The easy way out is the rule of zero: hold values (std::vector<Item>, not a vector of pointers) and declare none of the five, and the compiler's copy is already deep. With a polymorphic type, a copy through a base pointer needs a virtualclone()that returnsunique_ptr<Base>, because copying through the base would slice. - JavaScript's
structuredClonereturns plain data. A class instance comes back as a plain object without its methods, which is why the cart above is plain data with functions beside it. For class instances write aclone()method, as the template registry below does.
Two small programs check the claims that are easiest to get wrong: Python's deepcopy of an object that holds a collaborator, and JavaScript's spread, Object.assign and structuredClone on a class instance:
# deepcopy_pitfall.py: deepcopy duplicates a collaborator the object only uses
import copy
class PriceList: # shared by the whole app
pass
class Cart:
def __init__(self, prices: PriceList, items: list) -> None:
self.prices, self.items = prices, items
def __deepcopy__(self, memo): # copy what the cart owns, share what it uses
clone = copy.copy(self)
memo[id(self)] = clone # register first, so a cycle back to this cart finds the clone
clone.items = copy.deepcopy(self.items, memo)
return clone
class NaiveCart:
def __init__(self, prices: PriceList, items: list) -> None:
self.prices, self.items = prices, items
prices = PriceList()
print("plain deepcopy keeps the app's price list:", copy.deepcopy(NaiveCart(prices, [])).prices is prices)
print("with __deepcopy__ it keeps it:", copy.deepcopy(Cart(prices, ["pizza"])).prices is prices)plain deepcopy keeps the app's price list: False
with __deepcopy__ it keeps it: True// structured_clone.js: spread and Object.assign are shallow; none of the three keeps a class instance's methods
class Cart {
constructor(items) { this.items = items; }
total() { return this.items.reduce((sum, i) => sum + i.qty * i.price, 0); }
}
const cart = new Cart([{ name: "pizza", qty: 1, price: 300 }]);
const spread = { ...cart };
const assigned = Object.assign({}, cart);
const cloned = structuredClone(cart);
console.log("spread: total is a function?", typeof spread.total === "function", "| same items array?", spread.items === cart.items);
console.log("Object.assign: total is a function?", typeof assigned.total === "function", "| same items array?", assigned.items === cart.items);
console.log("structuredClone: total is a function?", typeof cloned.total === "function", "| same items array?", cloned.items === cart.items);
let error = "none";
try {
structuredClone({ f() {} });
} catch (e) {
error = e.name;
}
console.log("a Date survives:", structuredClone({ at: new Date(0) }).at instanceof Date, "| a function throws:", error);spread: total is a function? false | same items array? true
Object.assign: total is a function? false | same items array? true
structuredClone: total is a function? false | same items array? false
a Date survives: true | a function throws: DataCloneErrorCopy what the object owns, and share what it only uses. A cart owns its items, but it only uses the app's price list.
Better than copying: make the items immutable#
There is a third answer, and it is often the best one. The reorder bug needed two things: a shared object, and something that could change it. Remove the second, and the sharing is harmless. If an Item cannot change, a reorder can share the items it does not touch and replace only the one that changes:
# immutable_items.py: frozen items are safe to share; a changed copy replaces only what changed
from dataclasses import dataclass, replace
from typing import Tuple
@dataclass(frozen=True)
class Item:
name: str
qty: int
price: int
@dataclass(frozen=True)
class Cart:
items: Tuple[Item, ...]
note: str
def total(self) -> int:
return sum(i.qty * i.price for i in self.items)
def describe(self) -> str:
return ", ".join(f"{i.name} x{i.qty}" for i in self.items) + f" = {self.total()}"
def with_qty(self, name: str, qty: int) -> "Cart": # a changed copy: only the changed item is new
return replace(self, items=tuple(replace(i, qty=qty) if i.name == name else i for i in self.items))
def with_note(self, note: str) -> "Cart":
return replace(self, note=note)
past = Cart((Item("pizza", 1, 300), Item("cola", 2, 40)), "ring the bell")
print("past order:", past.describe())
reorder = past.with_qty("cola", 3).with_note("leave at gate")
print("reorder, cola raised to 3:", reorder.describe())
print("past order now:", past.describe())
print(f'notes: past "{past.note}", reorder "{reorder.note}"')
print("shared item objects:", sum(a is b for a, b in zip(past.items, reorder.items)), "of", len(past.items))// ImmutableItems.java: records are safe to share; a changed copy replaces only what changed
import java.util.List;
import java.util.stream.Collectors;
class ImmutableItems {
record Item(String name, int qty, int price) {}
record Cart(List<Item> items, String note) {
Cart { items = List.copyOf(items); } // an unmodifiable list of unmodifiable items
int total() { return items.stream().mapToInt(i -> i.qty() * i.price()).sum(); }
String describe() {
return items.stream().map(i -> i.name() + " x" + i.qty()).collect(Collectors.joining(", ")) + " = " + total();
}
Cart withQty(String name, int qty) { // a changed copy: only the changed item is new
return new Cart(items.stream().map(i -> i.name().equals(name) ? new Item(i.name(), qty, i.price()) : i).toList(), note);
}
Cart withNote(String text) { return new Cart(items, text); }
}
public static void main(String[] args) {
Cart past = new Cart(List.of(new Item("pizza", 1, 300), new Item("cola", 2, 40)), "ring the bell");
System.out.println("past order: " + past.describe());
Cart reorder = past.withQty("cola", 3).withNote("leave at gate");
System.out.println("reorder, cola raised to 3: " + reorder.describe());
System.out.println("past order now: " + past.describe());
System.out.println("notes: past \"" + past.note() + "\", reorder \"" + reorder.note() + "\"");
int shared = 0;
for (int i = 0; i < past.items().size(); i++) if (past.items().get(i) == reorder.items().get(i)) shared++;
System.out.println("shared item objects: " + shared + " of " + past.items().size());
}
}// immutable_items.cpp: const items are safe to share; a changed copy replaces only what changed
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
struct Item {
const std::string name;
const int qty;
const int price;
};
class Cart {
std::vector<std::shared_ptr<const Item>> items_; // pointers to const Items: copying the cart shares them, safely
std::string note_;
public:
Cart(std::vector<std::shared_ptr<const Item>> items, std::string note) : items_(std::move(items)), note_(std::move(note)) {}
const std::string& note() const { return note_; }
const std::vector<std::shared_ptr<const Item>>& items() const { return items_; }
int total() const {
int sum = 0;
for (const auto& i : items_) sum += i->qty * i->price;
return sum;
}
std::string describe() const {
std::string text;
for (std::size_t k = 0; k < items_.size(); ++k) text += (k ? ", " : "") + items_[k]->name + " x" + std::to_string(items_[k]->qty);
return text + " = " + std::to_string(total());
}
Cart with_qty(const std::string& name, int qty) const { // a changed copy: only the changed item is new
std::vector<std::shared_ptr<const Item>> changed;
for (const auto& i : items_) changed.push_back(i->name == name ? std::make_shared<const Item>(Item{i->name, qty, i->price}) : i);
return Cart(changed, note_);
}
Cart with_note(const std::string& text) const { return Cart(items_, text); }
};
int main() {
Cart past({std::make_shared<const Item>(Item{"pizza", 1, 300}), std::make_shared<const Item>(Item{"cola", 2, 40})}, "ring the bell");
std::cout << "past order: " << past.describe() << "\n";
Cart reorder = past.with_qty("cola", 3).with_note("leave at gate");
std::cout << "reorder, cola raised to 3: " << reorder.describe() << "\n";
std::cout << "past order now: " << past.describe() << "\n";
std::cout << "notes: past \"" << past.note() << "\", reorder \"" << reorder.note() << "\"\n";
int shared = 0;
for (std::size_t i = 0; i < past.items().size(); ++i) shared += past.items()[i] == reorder.items()[i] ? 1 : 0;
std::cout << "shared item objects: " << shared << " of " << past.items().size() << "\n";
}// immutableItems.js: frozen items are safe to share; a changed copy replaces only what changed
const item = (name, qty, price) => Object.freeze({ name, qty, price });
const cart = (items, note) => Object.freeze({ items: Object.freeze([...items]), note });
const total = (c) => c.items.reduce((sum, i) => sum + i.qty * i.price, 0);
const describe = (c) => c.items.map((i) => `${i.name} x${i.qty}`).join(", ") + ` = ${total(c)}`;
const withQty = (c, name, qty) => cart(c.items.map((i) => (i.name === name ? item(i.name, qty, i.price) : i)), c.note); // only the changed item is new
const withNote = (c, note) => cart(c.items, note);
const past = cart([item("pizza", 1, 300), item("cola", 2, 40)], "ring the bell");
console.log("past order:", describe(past));
const reorder = withNote(withQty(past, "cola", 3), "leave at gate");
console.log("reorder, cola raised to 3:", describe(reorder));
console.log("past order now:", describe(past));
console.log(`notes: past "${past.note}", reorder "${reorder.note}"`);
console.log("shared item objects:", past.items.filter((it, k) => it === reorder.items[k]).length, "of", past.items.length);past order: pizza x1, cola x2 = 380
reorder, cola raised to 3: pizza x1, cola x3 = 420
past order now: pizza x1, cola x2 = 380
notes: past "ring the bell", reorder "leave at gate"
shared item objects: 1 of 2The same totals, the past order is safe again, and 1 of 2 items is shared: the pizza, which nobody changed, is the same object in both carts, and only the cola was replaced. Sharing the pizza is harmless because nothing can change it. This is the cheapest copy there is (no deep copy, no clone), and it is what dataclasses.replace, a Java "wither" and the spread syntax do. Databases use the same idea: with MVCC a write leaves the old version of a row readable for transactions that started earlier, so a reader's snapshot never changes underneath it. The cost is that every change allocates a new object, and that C++ needs const members and shared_ptr<const T>, as above.
If an item cannot change, a reorder can share the items it does not touch and replace only the one that changes, with no deep copy at all.
Prototype proper: clone from a template#
So far the copy was a means to an end. The book's pattern goes further: you keep a template and create many objects by cloning it. A calendar app does this for "duplicate event": the template has a title, attendees and reminders, and each new event is a copy with a different time.
# prototype_registry.py: named templates; create() clones one and sets what differs
import copy
from typing import Dict, List
class Event:
def __init__(self, title: str, when: str, attendees: List[str], reminders: List[int]) -> None:
self.title, self.when, self.attendees, self.reminders = title, when, attendees, reminders
def clone(self) -> "Event":
return copy.deepcopy(self) # the lists are copied, so the copy never shares them with the template
def describe(self) -> str:
return f"{self.title} | {self.when} | {', '.join(self.attendees)} | reminders {', '.join(str(r) for r in self.reminders)}"
class TemplateRegistry: # the prototype manager
def __init__(self) -> None:
self._templates: Dict[str, Event] = {}
def register(self, name: str, event: Event) -> None:
self._templates[name] = event
def create(self, name: str, when: str) -> Event:
event = self._templates[name].clone() # never hand out the template itself
event.when = when
return event
standup = Event("team sync", "Mon 09:00", ["asha", "ravi"], [10, 1])
registry = TemplateRegistry()
registry.register("standup", standup)
print("template standup:", standup.describe())
tuesday = registry.create("standup", "Tue 09:30")
tuesday.attendees.append("meera") # editing one copy...
tuesday.reminders.append(5)
print("created:", tuesday.describe())
print("created:", registry.create("standup", "Wed 09:00").describe()) # ...does not leak into the next
print("template unchanged:", standup.describe())// PrototypeRegistry.java: named templates; create() clones one and sets what differs
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class PrototypeRegistry {
static final class Event {
final String title;
String when;
final List<String> attendees;
final List<Integer> reminders;
Event(String title, String when, List<String> attendees, List<Integer> reminders) {
this.title = title;
this.when = when;
this.attendees = attendees;
this.reminders = reminders;
}
Event(Event other) { // a copy constructor: the lists are copied, so the copy never shares them
this(other.title, other.when, new ArrayList<>(other.attendees), new ArrayList<>(other.reminders));
}
Event copy() { return new Event(this); } // named copy(), not clone(), to stay clear of Object.clone()
String describe() {
return title + " | " + when + " | " + String.join(", ", attendees) + " | reminders "
+ reminders.stream().map(String::valueOf).collect(Collectors.joining(", "));
}
}
static final class TemplateRegistry { // the prototype manager
private final Map<String, Event> templates = new HashMap<>();
void register(String name, Event event) { templates.put(name, event); }
Event create(String name, String when) {
Event event = templates.get(name).copy(); // never hand out the template itself
event.when = when;
return event;
}
}
public static void main(String[] args) {
Event standup = new Event("team sync", "Mon 09:00", new ArrayList<>(List.of("asha", "ravi")), new ArrayList<>(List.of(10, 1)));
TemplateRegistry registry = new TemplateRegistry();
registry.register("standup", standup);
System.out.println("template standup: " + standup.describe());
Event tuesday = registry.create("standup", "Tue 09:30");
tuesday.attendees.add("meera"); // editing one copy...
tuesday.reminders.add(5);
System.out.println("created: " + tuesday.describe());
System.out.println("created: " + registry.create("standup", "Wed 09:00").describe()); // ...does not leak into the next
System.out.println("template unchanged: " + standup.describe());
}
}// prototype_registry.cpp: named templates; create() copies one and sets what differs
#include <iostream>
#include <map>
#include <string>
#include <vector>
struct Event { // all members are values, so the compiler's copy is already a deep copy (the rule of zero)
std::string title;
std::string when;
std::vector<std::string> attendees;
std::vector<int> reminders;
std::string describe() const {
std::string text = title + " | " + when + " | ";
for (std::size_t i = 0; i < attendees.size(); ++i) text += (i ? ", " : "") + attendees[i];
text += " | reminders ";
for (std::size_t i = 0; i < reminders.size(); ++i) text += (i ? ", " : "") + std::to_string(reminders[i]);
return text;
}
};
class TemplateRegistry { // the prototype manager
std::map<std::string, Event> templates_;
public:
void add(const std::string& name, const Event& event) { templates_[name] = event; }
Event create(const std::string& name, const std::string& when) const {
Event event = templates_.at(name); // the copy constructor copies both vectors: never the template itself
event.when = when;
return event;
}
};
int main() {
Event standup{"team sync", "Mon 09:00", {"asha", "ravi"}, {10, 1}};
TemplateRegistry registry;
registry.add("standup", standup);
std::cout << "template standup: " << standup.describe() << "\n";
Event tuesday = registry.create("standup", "Tue 09:30");
tuesday.attendees.push_back("meera"); // editing one copy...
tuesday.reminders.push_back(5);
std::cout << "created: " << tuesday.describe() << "\n";
std::cout << "created: " << registry.create("standup", "Wed 09:00").describe() << "\n"; // ...does not leak into the next
std::cout << "template unchanged: " << standup.describe() << "\n";
}// prototypeRegistry.js: named templates; create() clones one and sets what differs
class Event {
constructor(title, when, attendees, reminders) {
this.title = title;
this.when = when;
this.attendees = attendees;
this.reminders = reminders;
}
clone() { // structuredClone would drop the methods, so copy the lists by hand
return new Event(this.title, this.when, [...this.attendees], [...this.reminders]);
}
describe() {
return `${this.title} | ${this.when} | ${this.attendees.join(", ")} | reminders ${this.reminders.join(", ")}`;
}
}
class TemplateRegistry { // the prototype manager
#templates = new Map();
register(name, event) { this.#templates.set(name, event); }
create(name, when) {
const event = this.#templates.get(name).clone(); // never hand out the template itself
event.when = when;
return event;
}
}
const standup = new Event("team sync", "Mon 09:00", ["asha", "ravi"], [10, 1]);
const registry = new TemplateRegistry();
registry.register("standup", standup);
console.log("template standup:", standup.describe());
const tuesday = registry.create("standup", "Tue 09:30");
tuesday.attendees.push("meera"); // editing one copy...
tuesday.reminders.push(5);
console.log("created:", tuesday.describe());
console.log("created:", registry.create("standup", "Wed 09:00").describe()); // ...does not leak into the next
console.log("template unchanged:", standup.describe());template standup: team sync | Mon 09:00 | asha, ravi | reminders 10, 1
created: team sync | Tue 09:30 | asha, ravi, meera | reminders 10, 1, 5
created: team sync | Wed 09:00 | asha, ravi | reminders 10, 1
template unchanged: team sync | Mon 09:00 | asha, ravi | reminders 10, 1Read the output. The Tuesday event added an attendee and a reminder, and the Wednesday event, made from the same template afterwards, has neither, and neither does the template. That is the whole contract of clone(): a copy that nobody else shares. Strings and numbers are immutable, so copying each list is enough. The C++ version needed no copy code at all, because its members are values (std::vector copies its contents). A registry of ready-made templates is a small cache, and the usual cache rule applies: hand out copies, not the cached object.
When is cloning better than constructing?
Construct (a plain constructor, or a builder) when
- The object is small, or cheap to build from its parts
- Each new object differs in most fields
- You can write the fields down in one line (keyword arguments, a record)
- You are tempted to clone only to avoid typing
Clone from a template when
- Building is expensive: it reads a file, parses a document, queries a database or loads an image, and the template is already in memory
- The object has many fields, and a new one differs in two or three
- The state is configured at run time, so there is no class for it to come from: users save their own templates
- You need a snapshot: the exact state now, to compare or restore later
Clone it, or construct it?
A settings screen lets a rider save "my usual ride" (pickup, drop, one stop, a note) and book it again with one tap. Do you clone a saved ride or construct a new one from its fields?
One strong answer
Either works, and the deciding question is what the object holds. If a ride is a small immutable value, "book it again" is a copy with a new time: no clone, no registry, just replace(saved, when=now). Reach for Prototype if building a ride is expensive or if the saved object holds mutable parts (a list of stops that the new ride will edit), and then copy the lists. Sharing a mutable list between the saved ride and the booked one is the shallow-copy bug again.
Clone from a template when building is expensive, when a new object differs in only a few fields, or when users save their own templates. Otherwise construct.
Prototype and its look-alikes#
| Idea | What it is | How to tell it from Prototype |
|---|---|---|
| Copy constructor | A constructor that takes another object of the same class (Cart(Cart other)) |
The mechanism a clone() is often written with, but the caller must name the class. Prototype's clone() is called through the interface, so the caller copies without knowing the class |
| Memento / snapshot | Capture the state to restore it later | The same copy, a different purpose: a snapshot is kept for restoring, a prototype for creating more objects. The text editor takes snapshots |
Copy with a change (replace, a wither, the spread) |
A new object with one field different | Often all you need for an immutable value. Prototype is for objects you cannot or do not want to rebuild |
| Flyweight | Share one immutable object among many users | What the immutable-items design does, on a small scale. The opposite of copying: share, do not copy |
| Factory | Builds an object from scratch, often by class | A factory can use prototypes: each "create" copies a template. The book notes that an Abstract Factory can be built from prototypes |
| Builder | Assembles one object over many calls | A builder starts from nothing. A prototype starts from a complete object. A builder can be reused as a template, which is Prototype-like |
| Serialisation round trip | Write to JSON or bytes and read back | A way to deep copy, and slow. Loses types (Date becomes a string in JSON) and cannot copy a function or a lock |
When not to use it#
- When a constructor or a keyword-argument call is just as short. A clone that forgets a field produces a wrong object quietly.
- When the object is immutable. Share it, and use "copy with a change" for the one field that differs. A deep copy of something that cannot change is wasted work.
- When you copy a graph with shared parts or cycles by hand. A hand-written deep copy must copy a shared object once, as
deepcopyandstructuredClonedo. Hand-written code usually does not. - When the object holds a lock, socket, connection or logger. Those belong to the app. Copy the data, and share or re-create the resource.
- When a template is handed out directly. One caller's edit then changes it for every later caller. Return a clone.
A template registry must hand out clones. If it returns the stored template, one caller's edit changes it for every later caller.
Where it shows up in the case studies#
No case study uses Prototype by name. Three places are close enough to read in its light:
| Case study | What | How |
|---|---|---|
| Text editor | Snapshots are copies | Editor.save() returns a Snapshot(text, cursor): the document's state at that moment, kept to restore later. The lesson measures it against Command: 100 snapshots of a 50,000-character document hold about 5 million characters. Python strings are immutable, so holding the string is a complete copy |
| Meeting scheduler | A series from one booking | book(..., repeat=Recurrence) constructs one Meeting per slot (built from the same values, not cloned) with the same title, organiser and attendees, and gives each meeting its own invites dictionary (the dict is built inside the loop). If the occurrences shared one dictionary, an RSVP to one Tuesday would answer every Tuesday: the shared-parts bug of this lesson |
| Key-value store | A point-in-time copy | snapshot() copies the live keys into a new read-only mapping while holding the lock. A shallow copy is enough because the values are strings, which cannot change |
In the interview#
Say it with the danger: "Prototype is making a new object by copying an existing one, usually from a registry of templates. The thing to get right is shallow versus deep: copy what the new object owns, share what it only uses, and if the parts are immutable, share them and just replace what changes."
Then expect the follow-ups:
- "What is the difference between a shallow and a deep copy?" A shallow copy duplicates the object and copies its references, so nested objects are shared. A deep copy duplicates everything the object owns, recursively.
- "How do you deep copy in Python?"
copy.deepcopy, with__deepcopy__when the object holds something it should share.copy.copyis shallow. - "Why is Java's
clone()considered broken?"Cloneableis a marker with no method,Object.clone()is protected and throws a checked exception, it is shallow, it skips constructors and it cannot reassign afinalfield. Prefer a copy constructor or a copy factory. - "What is the rule of three, five and zero in C++?" A custom destructor, copy constructor or copy assignment usually means all three; with C++11 moves, all five. Better: hold values, declare none, and the default copy is correct.
- "What does
structuredClonedo that spread does not?" It copies deeply, including cycles,Date,MapandSet. Neither keeps a class's methods, andstructuredClonethrows on a function. - "Why does a template registry return clones?" If it returned the stored object, one caller's edit would change the template for everyone after.
Common mistakes#
- Calling
copy.copy,clone()or spread a "deep copy". They are shallow. deepcopyon an object that holds a service or a lock, so the service is duplicated, or the call raises.- Writing a Java
clone()that callssuper.clone()and forgets the mutable fields. - A C++ class with a raw or shared pointer member and the default copy, so two objects share what each thought it owned.
- Writing a C++ destructor and no copy constructor (the rule of three): the compiler's copy now shares or double-frees.
- Expecting
structuredCloneor spread to keep a class instance's methods. They give you plain data.
Prototype
Q1/4 A reorder copies a cart with copy.copy, then raises one item's quantity. The past order changes too. What is the cause?
Answer & explanation
Answer: C
copy.copy makes a new cart but copies its references, so both carts point at the same Item objects. Replacing a field on the copy is safe, while changing a shared item is not. deepcopy, or immutable items, fixes it.
Q2/4 A Python Cart holds a reference to the app's shared PriceList. Calling copy.deepcopy(cart) does what?
Answer & explanation
Answer: B
deepcopy copies everything reachable, including a collaborator the object only uses. A __deepcopy__ method can copy the owned parts and keep sharing the service.
Q3/4 In C++, a class has a std::vector<std::shared_ptr<Item>> member and no copy constructor. What does copying it do?
Answer & explanation
Answer: C
The compiler's copy is memberwise. A vector copies its elements, but each element is a shared_ptr, which copies as a pointer. Hold the Items by value (the rule of zero), or write the copy constructor.
Q4/4 When is it better to share an object than to clone it?
Answer & explanation
Answer: C
A shared object is only dangerous if something can change it. Immutable values can be shared freely.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


