Prototype — Middle Level¶
Source: refactoring.guru/design-patterns/prototype Prerequisite: Junior Focus: Why and When
Table of Contents¶
- Introduction
- When to Use Prototype
- When NOT to Use Prototype
- Real-World Cases
- Production Code
- Trade-offs
- Alternatives
- Refactoring
- Edge Cases
- Tricky Points
- Best Practices
- Summary
- Diagrams
Introduction¶
Focus: Why and When
Prototype is the right tool when building from scratch is expensive but copying is cheap — or when you need to clone polymorphically without knowing the concrete type. The middle-level skill is identifying these scenarios and choosing the right copy depth (shallow vs deep) and architecture (registry, factory, hybrid).
The pattern's strongest case is configuration presets: build N "templates" once, clone them for actual use. Examples: game enemies, document templates, API request defaults.
When to Use Prototype¶
Use Prototype when any of:
- Construction is expensive. Loading from disk, parsing schemas, computing initial state.
- Polymorphic copying needed. Code has a
Shapereference; needs another shape just like it without knowing its concrete class. - Configuration variants. A "default" object plus modifications.
- Snapshots. Memento-like state capture (though Memento is the better fit there).
- Many similar objects. Game entities, particles, document elements.
Strong-fit examples¶
- Game engines (entity templates).
- Document editors (default styled blocks).
- Test fixtures (modify a copy of valid data).
- Preset configurations (dev/staging/prod from base).
When NOT to Use Prototype¶
| Anti-pattern symptom | Better choice |
|---|---|
| Construction is cheap | Just use constructor |
| Object is immutable | Share the same instance — no clone needed |
| Deep clone gets tangled (cycles, external resources) | Reconsider design |
| You don't actually clone — just create variants | Factory Method or Builder |
Strong-misfit examples¶
- Simple value types (
Point,Money) — share, don't clone. - Singletons — defeats the pattern.
- Objects with file handles, network connections, threads.
Real-World Cases¶
1. Game Entities¶
# Pre-configure prototypes once
goblin_proto = Enemy(hp=30, speed=5, sprite="goblin.png", attack=Attack("bite", 5))
orc_proto = Enemy(hp=80, speed=3, sprite="orc.png", attack=Attack("club", 12))
# In game loop
for spawn_point in level.spawns:
enemy = copy.deepcopy(goblin_proto)
enemy.x, enemy.y = spawn_point
level.enemies.append(enemy)
Building 100 enemies from scratch loads 100 sprites, parses 100 attack configs. Cloning the prototype reuses parsed data.
2. Document Editor — Style Templates¶
StyledBlock heading1 = new StyledBlock(font: "Arial", size: 24, bold: true, color: black);
StyledBlock heading2 = heading1.clone();
heading2.size = 20;
Derive variants from a "Heading 1" template.
3. Multi-Environment Configs¶
base_config = AppConfig.load("base.yaml")
dev_config = copy.deepcopy(base_config); dev_config.debug = True
staging_config = copy.deepcopy(base_config); staging_config.cache_size = 1000
prod_config = copy.deepcopy(base_config); prod_config.cache_size = 10000
Same starting point; small per-environment tweaks.
4. Test Data¶
User valid = User.builder().name("Test").email("t@e.c").role("user").build();
@Test
void adminCanDelete() {
User admin = clone(valid);
admin.role = "admin";
// ...
}
(Or use Test Data Builder for the same effect.)
5. Undo/Redo via Snapshots¶
type Editor struct {
state *State
history []*State
}
func (e *Editor) Undo() {
if len(e.history) == 0 { return }
e.state = e.history[len(e.history)-1].Clone()
e.history = e.history[:len(e.history)-1]
}
func (e *Editor) Save() {
e.history = append(e.history, e.state.Clone())
}
Each save snapshots state via clone. (Note: Memento is the dedicated pattern for this.)
Production Code¶
Java — Avoiding Cloneable¶
public final class Document {
private final String title;
private final List<Section> sections;
private final Map<String, String> metadata;
public Document(String title, List<Section> sections, Map<String, String> metadata) {
this.title = title;
this.sections = new ArrayList<>(sections); // defensive copy
this.metadata = new HashMap<>(metadata);
}
/** Copy constructor — deep copy of nested mutable state. */
public Document(Document other) {
this.title = other.title;
this.sections = other.sections.stream().map(Section::new).collect(Collectors.toList());
this.metadata = new HashMap<>(other.metadata);
}
public Document clone() { return new Document(this); }
}
public final class Section {
public final String content;
public final List<String> tags;
public Section(String content, List<String> tags) { this.content = content; this.tags = new ArrayList<>(tags); }
public Section(Section other) { this.content = other.content; this.tags = new ArrayList<>(other.tags); }
}
Each level handles its own deep copy. No Cloneable.
Python — Custom __deepcopy__ for Performance¶
import copy
from dataclasses import dataclass, field
@dataclass
class CachedDocument:
title: str
cache: dict[str, bytes] = field(default_factory=dict) # large
sections: list = field(default_factory=list)
def __deepcopy__(self, memo):
# Don't deep-copy the cache (immutable bytes anyway, share is fine)
new = CachedDocument(self.title)
new.cache = self.cache # shared
new.sections = copy.deepcopy(self.sections, memo)
return new
doc1 = CachedDocument(title="A", cache={"img": b"..." * 10**6})
doc2 = copy.deepcopy(doc1)
# doc1.cache is doc2.cache (shared, saves memory)
Custom __deepcopy__ lets you mix shallow + deep selectively. Used when some fields are large + immutable and shouldn't be deep-copied.
Go — Hierarchical Clone with Slices/Maps¶
package config
type Database struct {
Host string
Port int
Tags []string
}
func (d *Database) Clone() *Database {
return &Database{
Host: d.Host,
Port: d.Port,
Tags: append([]string(nil), d.Tags...), // explicit slice copy
}
}
type AppConfig struct {
Name string
Database *Database
Features map[string]bool
}
func (c *AppConfig) Clone() *AppConfig {
features := make(map[string]bool, len(c.Features))
for k, v := range c.Features {
features[k] = v
}
return &AppConfig{
Name: c.Name,
Database: c.Database.Clone(), // recursive
Features: features,
}
}
Each level explicitly handles its own deep copy. No reflection, no copy package magic.
Trade-offs¶
| Dimension | Prototype (clone) | Constructor + state | Builder | Factory |
|---|---|---|---|---|
| Concrete class dependency | None | Required | Required | None |
| Construction cost | Cheap (copy) | Recomputed | Cheap-ish | Recomputed |
| Variant derivation | Easy | Manual | Easy | Manual |
| Risk | Shallow/deep bugs | None | None | None |
Alternatives¶
vs Builder¶
- Builder for assembling from scratch with many optional fields.
- Prototype for copying an existing configured object.
Combine: a Builder produces a prototype; clones derive variants.
vs Factory Method¶
- Factory decides which class to instantiate.
- Prototype clones existing instance, doesn't choose a class.
Use Prototype when construction is expensive; Factory when you need polymorphic instantiation.
vs Sharing Immutable¶
For immutable objects, just share the reference:
TIMEOUT = Duration(seconds=30)
client.request(timeout=TIMEOUT) # safe to share — Duration is immutable
Cloning immutable is wasteful.
vs Memento¶
Memento is for state snapshots (undo/redo), preserving encapsulation. Prototype is for "make me another like this." Different intents, similar mechanics.
Refactoring¶
Toward Prototype¶
Code with repeated expensive construction:
# Before
for i in range(1000):
enemy = Enemy(load_sprite("goblin.png"), parse_attack("bite"), ...)
spawn(enemy)
# After
goblin_proto = Enemy(load_sprite("goblin.png"), parse_attack("bite"), ...)
for i in range(1000):
enemy = copy.deepcopy(goblin_proto)
spawn(enemy)
Away from Prototype¶
If the cloned objects are immutable and small, just share:
Edge Cases¶
Circular References¶
Naive deep clone infinite-loops:
a = Node(); b = Node()
a.parent = b; b.parent = a
copy.deepcopy(a) # works! `copy.deepcopy` uses memo internally.
Custom __deepcopy__ must accept and use memo:
def __deepcopy__(self, memo):
if id(self) in memo: return memo[id(self)]
new = type(self)(); memo[id(self)] = new
new.parent = copy.deepcopy(self.parent, memo)
return new
External Resources¶
File handles, sockets, threads can't be cloned naively. Either reopen in clone, or share carefully.
public Document clone() {
Document d = new Document(this);
d.fileHandle = openNewFile(this.path); // fresh handle
return d;
}
Final Fields (Java)¶
A copy constructor handles final fields naturally. Object.clone() does not (it skips constructor invocation).
Inheritance Chains¶
Each level of inheritance must override clone() (or copy constructor). Forgetting to do so leads to slicing — clone returns parent type, losing subclass fields.
Tricky Points¶
- Java's
Cloneableis broken. Always implement clone via copy constructor. - Python's default
copy.deepcopyworks for most classes. Override only when needed (large immutable fields, external resources). - Go's slice/map
=is shallow. Always explicit copy inClone(). - Prototype + Singleton conflict. Cloning a singleton makes two of them — defeats the singleton pattern.
- Performance: deep clone of large object graphs is O(n). For very large trees, consider persistent data structures (structural sharing).
Best Practices¶
- Prefer copy constructor over
Cloneablein Java. - Document shallow vs deep semantics clearly.
- In Go, explicitly copy slices/maps; don't trust
=. - In Python,
copy.deepcopyis the default tool; override__deepcopy__for performance. - Combine with Registry for "preset" prototypes.
- Don't clone immutable objects — share references.
- Test: verify clone is independent (mutate clone, original unchanged).
Summary¶
- Prototype = copy existing objects without concrete-class dependency.
- Use when construction is expensive or polymorphic copy is needed.
- Java: copy constructor + clone (skip
Cloneable). - Python:
copy.copy/copy.deepcopywith optional overrides. - Go: explicit deep-copy of reference fields.
- Often combined with Registry for preset templates.
Diagrams¶
Shallow vs Deep¶
Refactor¶
← Junior · Creational · Roadmap · Next: Senior