Prototype — Senior Level¶
Source: refactoring.guru/design-patterns/prototype Prerequisites: Junior · Middle
Introduction¶
Prototype at the senior level intersects with memory, performance, and cross-cutting concerns: the cost of deep clones, structural sharing tricks, copy-on-write semantics, and prototype-based architecture decisions (registry pattern, prototype + factory composition).
Architectural Patterns¶
Prototype + Registry¶
public final class PrototypeRegistry {
private final Map<String, Prototype> registry = new ConcurrentHashMap<>();
public void register(String name, Prototype p) { registry.put(name, p); }
public Prototype create(String name) {
Prototype p = registry.get(name);
if (p == null) throw new IllegalArgumentException(name);
return p.clone();
}
}
The registry is essentially a typed factory whose backing implementation is "look up the prototype, clone it." Adding a variant = registering a new prototype, no new class.
Prototype + Abstract Factory¶
Abstract Factory's products may be Prototypes:
class ButtonProtoFactory {
private final Button winProto = new WindowsButton(/* configured */);
public Button create() { return winProto.clone(); }
}
Construction reduces to a clone — useful when products are heavy.
Prototype + Composite¶
Cloning a composite tree: each node clones itself + recursively clones children.
class TreeNode:
def __init__(self): self.children = []
def __deepcopy__(self, memo):
new = TreeNode()
memo[id(self)] = new
new.children = [copy.deepcopy(c, memo) for c in self.children]
return new
Recursive clone is natural for trees.
Prototype-based Configuration¶
class FeatureFlags:
def __init__(self):
self.debug = False
self.experimental = {}
def __deepcopy__(self, memo):
new = FeatureFlags()
new.debug = self.debug
new.experimental = self.experimental.copy()
return new
# Base prototype
base = FeatureFlags()
base.debug = False
# Per-tenant variants
tenant_a = copy.deepcopy(base)
tenant_a.experimental["new-ui"] = True
Performance Considerations¶
Cost of Deep Clone¶
Deep clone is O(n) in object graph size. For 1M-node tree: hundreds of microseconds.
Mitigations¶
- Selective deep copy. Identify which fields really need fresh copies; share immutable.
- Persistent data structures. Scala/Clojure immutable maps share structure; "copy" is O(log n) for maps, O(1) for lists with cons.
- Copy-on-Write. Treat the clone as read-only; only allocate fresh memory when modified.
Copy-on-Write (COW)¶
class CowList:
def __init__(self, source: list = None):
self._items = source if source is not None else []
self._owned = source is None
def append(self, item):
if not self._owned:
self._items = list(self._items) # copy now
self._owned = True
self._items.append(item)
Until first mutation, the "clone" shares storage. First mutation copies.
Used in OS kernels (fork()), Linux's mmap, Java's CopyOnWriteArrayList.
Persistent Data Structures (Scala)¶
m2 shares most internal nodes with m1. "Copy" is O(log n), not O(n).
Concurrency¶
Cloning under contention¶
If many threads clone the same prototype simultaneously, the prototype's read access must be safe. Make prototypes immutable:
public final class ImmutablePrototype {
private final List<String> items; // unmodifiable
public ImmutablePrototype clone() {
// No synchronization needed — read-only access to items
return new ImmutablePrototype(new ArrayList<>(items));
}
}
If the prototype is mutable and may be modified mid-clone: synchronize.
Race conditions in registry¶
var (
registry = map[string]Prototype{}
mu sync.RWMutex
)
func Register(name string, p Prototype) {
mu.Lock(); defer mu.Unlock()
registry[name] = p
}
func Create(name string) Prototype {
mu.RLock(); defer mu.RUnlock()
return registry[name].Clone()
}
sync.RWMutex allows concurrent reads. Or use sync.Map for fully concurrent access.
Testability¶
@Test
void cloneIsIndependent() {
Document orig = makeDoc();
Document copy = orig.clone();
copy.title = "modified";
assertEquals("original", orig.title); // unchanged
}
@Test
void cloneIsDeep() {
Document orig = makeDoc();
orig.sections.add(new Section("A"));
Document copy = orig.clone();
copy.sections.add(new Section("B"));
assertEquals(1, orig.sections.size()); // unchanged
assertEquals(2, copy.sections.size());
}
Verify both: - Clone is logically equal to original. - Mutating clone doesn't affect original.
Code Examples — Advanced¶
Java — Copy-on-Write Prototype¶
public final class CowDocument {
private List<String> sections;
private boolean owned;
public CowDocument(List<String> sections) {
this.sections = sections;
this.owned = false; // we don't own this list yet
}
public CowDocument clone() {
// Cheap: share the list.
return new CowDocument(this.sections);
}
public void addSection(String s) {
if (!owned) {
this.sections = new ArrayList<>(sections);
this.owned = true;
}
sections.add(s);
}
}
clone() is O(1). Mutation triggers the actual copy.
Python — Memoized Deep Copy¶
class GraphNode:
def __init__(self, value):
self.value = value
self.neighbors: list[GraphNode] = []
def __deepcopy__(self, memo):
if id(self) in memo: return memo[id(self)]
new = GraphNode(self.value)
memo[id(self)] = new
new.neighbors = [copy.deepcopy(n, memo) for n in self.neighbors]
return new
memo correctly handles cycles in graphs.
Go — Selective Clone with Functional Options¶
type Config struct {
Database *DBConfig
Cache *CacheConfig
Logging *LogConfig
}
type CloneOption func(*Config)
func ShareDatabase(c *Config) { c.Database = origDB } // share, don't clone
func (c *Config) Clone(opts ...CloneOption) *Config {
cp := &Config{
Database: c.Database.Clone(),
Cache: c.Cache.Clone(),
Logging: c.Logging.Clone(),
}
for _, o := range opts { o(cp) }
return cp
}
// Usage
shared := orig.Clone(ShareDatabase) // Cache + Logging are deep, Database is shared
When Prototype Becomes a Liability¶
Symptom 1: Manual deep clone code grows complex¶
20 levels of nested fields, each requiring careful copy. Refactor to immutable data + structural sharing.
Symptom 2: Cloning shared resources¶
File handles, sockets, threads. Prototype is the wrong tool — use Factory + reopen.
Symptom 3: Cloning is overkill¶
If the source object is immutable, sharing it is enough. Prototype adds unnecessary copies.
Symptom 4: Prototype inheritance complexity¶
Each subclass must override clone() correctly. Mistakes lead to slicing. Modern languages with copy semantics (records, dataclasses) reduce this risk.
Migration Patterns¶
Prototype → Immutable + Structural Sharing¶
// Before: mutable + clone
class Config { ... def clone(): Config = ... }
// After: immutable case class
case class Config(...)
val variant = base.copy(timeout = 60) // structural sharing, free clone
Prototype → Factory¶
If "construction" isn't actually expensive, replace clone() with a Factory that builds fresh.
Prototype → DI Container¶
@Configuration
class AppConfig {
@Bean @Scope("prototype") // every injection gets a fresh instance
Doc doc() { return loadFromDisk(); }
}
Spring @Scope("prototype") is literally Prototype pattern in DI form.
Related Topics¶
- Next: Prototype — Professional
- Practice: Tasks, Find-Bug, Optimize, Interview.
- Companions: Memento, Composite, Builder, Singleton.
- Persistent data structures: Scala
immutable, Clojure data, Vavr.
← Middle · Creational · Roadmap · Next: Professional