Prototype
Intent
Section titled “Intent”Prototype creates new objects by copying an existing, fully configured instance — the prototype — rather than constructing one from scratch.
Problem
Section titled “Problem”Sometimes building an object from its constructor is expensive or awkward: it might require an expensive computation, a lot of configuration, or knowledge the caller does not have. If you already hold an instance that is almost what you want, copying it and tweaking the copy is simpler than rebuilding it. Prototype makes “give me one like this” a first-class operation: the object knows how to clone itself.
The decision that defines Prototype is shallow versus deep. A shallow copy duplicates the top-level object but shares its nested references, so mutating a nested field in the copy also changes the original. A deep copy duplicates the whole graph, so the copy is fully independent. Choosing wrong is a classic source of bugs — a “copy” that secretly shares a list with its source will surprise you the first time either side is mutated. Prototype forces you to make that choice on purpose.
Structure
Section titled “Structure”classDiagram
class Prototype {
<<interface>>
+clone() Prototype
}
class Document {
+title: string
+tags: List
+clone() Document
}
class Client {
+duplicate(p) Prototype
}
Prototype <|.. Document
Client ..> Prototype : clones - Prototype — the interface declaring a
cloneoperation. - Concrete Prototype (
Document) — implementscloneto produce a copy of itself, deciding shallow vs. deep. - Client — obtains new objects by cloning a prototype instead of calling a constructor.
Example
Section titled “Example”A Document with a list of tags. We clone it deeply so editing the copy’s tags leaves the original untouched.
class Document { constructor( public title: string, public tags: string[], ) {}
// Deep clone: structuredClone copies the nested array too. clone(): Document { const copy = structuredClone({ title: this.title, tags: this.tags }); return new Document(copy.title, copy.tags); }}
const original = new Document('Report', ['draft', 'q2']);const copy = original.clone();copy.title = 'Report (revised)';copy.tags.push('final');
console.log(original.title, original.tags); // Report ['draft', 'q2']console.log(copy.title, copy.tags); // Report (revised) ['draft', 'q2', 'final']import copyfrom dataclasses import dataclass, field
@dataclassclass Document: title: str tags: list[str] = field(default_factory=list)
def clone(self) -> "Document": # deepcopy duplicates the nested list, so copies are independent. return copy.deepcopy(self)
original = Document("Report", ["draft", "q2"])duplicate = original.clone()duplicate.title = "Report (revised)"duplicate.tags.append("final")
print(original.title, original.tags) # Report ['draft', 'q2']print(duplicate.title, duplicate.tags) # Report (revised) ['draft', 'q2', 'final']package main
import "fmt"
type Document struct { Title string Tags []string}
// Clone returns a deep copy: the tags slice is duplicated, not shared.func (d Document) Clone() Document { tags := make([]string, len(d.Tags)) copy(tags, d.Tags) return Document{Title: d.Title, Tags: tags}}
func main() { original := Document{Title: "Report", Tags: []string{"draft", "q2"}} duplicate := original.Clone() duplicate.Title = "Report (revised)" duplicate.Tags = append(duplicate.Tags, "final")
fmt.Println(original.Title, original.Tags) // Report [draft q2] fmt.Println(duplicate.Title, duplicate.Tags) // Report (revised) [draft q2 final]}// Deriving Clone gives a deep copy: the Vec<String> is duplicated.#[derive(Clone)]struct Document { title: String, tags: Vec<String>,}
fn main() { let original = Document { title: "Report".to_string(), tags: vec!["draft".to_string(), "q2".to_string()], };
let mut duplicate = original.clone(); duplicate.title = "Report (revised)".to_string(); duplicate.tags.push("final".to_string());
println!("{} {:?}", original.title, original.tags); // Report ["draft", "q2"] println!("{} {:?}", duplicate.title, duplicate.tags); // Report (revised) ["draft", "q2", "final"]}When to use / trade-offs
Section titled “When to use / trade-offs”- Pro: cheaper than rebuilding when an object is expensive or complicated to construct.
- Pro: the client copies an existing object without depending on its concrete class or constructor arguments.
- Pro: lets you keep a library of preconfigured prototypes and stamp out variations.
- Con: cloning object graphs with cycles or shared resources (open files, sockets) is tricky.
- Con: the shallow-versus-deep distinction is easy to get wrong, producing copies that secretly share state.
Related patterns
Section titled “Related patterns”- Builder constructs an object step by step, while Prototype starts from a ready-made one.
- Abstract Factory can store and return prototypes instead of constructing each product fresh.