Prototype and registries
Cloning a configured object, or looking one up by name.
01The intuition
The problem it solves
A game has monsters. Each kind of monster has dozens of settings: hit points, speed, sprite, sound, attack pattern, loot table. The first version builds every monster from scratch, repeating the settings.
def spawn(kind: str):
if kind == "goblin":
return Monster(hp=30, speed=4, sprite=load("goblin.png"), sound=load("grr.wav"),
attacks=[Stab(), Throw()], loot=["coin"])
if kind == "troll":
return Monster(hp=200, speed=1, sprite=load("troll.png"), sound=load("roar.wav"),
attacks=[Smash()], loot=["club", "coin"])
raise ValueError(kind)
Two problems hide here. load("goblin.png") runs every spawn, so spawning fifty goblins loads the same file fifty times. And a level designer who wants a "fire goblin" that is a goblin with a different sprite and more hit points has to copy the whole goblin line and keep it in sync by hand.
Two small ideas fix this together. A prototype is a fully configured example object that you clone instead of rebuilding. A registry is a dictionary from a name to the thing that makes objects, so new kinds can be added at runtime without editing a ladder.
Build it one piece at a time
Step 1: configure each kind once, as an object. The expensive loads happen a single time.
goblin_proto = Monster(hp=30, speed=4, sprite=load("goblin.png"), sound=load("grr.wav"),
attacks=[Stab(), Throw()], loot=["coin"])
troll_proto = Monster(hp=200, speed=1, sprite=load("troll.png"), sound=load("roar.wav"),
attacks=[Smash()], loot=["club", "coin"])
Step 2: give the object a clone that copies the right amount. Mutable state such as the loot list is copied; shared, unchanging assets such as the sprite are not. This decision is the heart of the pattern.
import copy
class Monster:
def __init__(self, hp, speed, sprite, sound, attacks, loot):
self.hp, self.speed, self.sprite, self.sound = hp, speed, sprite, sound
self.attacks, self.loot = attacks, loot
def clone(self) -> "Monster":
return Monster(self.hp, self.speed, self.sprite, self.sound,
list(self.attacks), list(self.loot))
Step 3: keep prototypes in a registry keyed by name. Spawning becomes a lookup and a clone, and the ladder is gone.
class MonsterRegistry:
def __init__(self):
self._protos: dict[str, Monster] = {}
def register(self, name: str, proto: Monster) -> None:
self._protos[name] = proto
def spawn(self, name: str) -> Monster:
try:
return self._protos[name].clone()
except KeyError:
raise ValueError(f"unknown monster {name}")
registry = MonsterRegistry()
registry.register("goblin", goblin_proto)
registry.register("troll", troll_proto)
Step 4: derive new kinds from existing ones. The fire goblin is a clone of the goblin with two fields changed, registered under a new name, possibly from a data file at startup.
fire = registry.spawn("goblin")
fire.hp, fire.sprite = 45, load("fire_goblin.png")
registry.register("fire_goblin", fire)
| Step | File loads per 50 goblins | Adding fire goblin edits | Two goblins share a loot list |
|---|---|---|---|
| Start | 100 | the ladder, a full line | no (rebuilt each time) |
| 1 | 2 | a copied line | yes, a bug |
| 2 and 3 | 2 | a registration | no (cloned) |
| 4 | 2 | three lines, no code in the game loop | no |
Why it works
A prototype turns configuration into data: the example object is the specification. Cloning copies the parts each instance will change and shares the parts nobody changes, which is both faster and smaller. The invariant is "a clone starts equal to its prototype and never affects it". The registry turns the set of kinds into a runtime table, so the code that spawns is closed to new kinds.
Where it breaks: a clone that shallow-copies a mutable field, so two goblins share one loot list and looting one empties the other; and deep-copying a large shared asset by accident, which makes spawning slow again. Decide field by field, and write the test that mutates a clone and checks the prototype.
The trade-off you must say out loud
A registry is a layer of indirection: to learn what "fire_goblin" is, you read a table or a data file instead of a class. That is a feature when designers, not programmers, add kinds, and a cost when there are three kinds that never change. Say which world you are in. The follow-up is often "what if a kind needs different behaviour, not just values", and the answer is that the registry then maps to a factory function or a class, the factory form, with prototypes kept for the data-only kinds.
Where it shows up in the 100 Google problems
- Merge two configuration trees with per-node rules: merge rules are registered by node type or path, and a new rule is a registration, which is how the problem stays open-ended.
- Design a rule and workflow engine: conditions and actions are looked up by name from a registry that plugins fill at startup.
- Design a notification service: message templates are prototypes cloned and filled per recipient; channels are a registry by name.
- Design a coffee machine: a recipe is a prototype of ingredient amounts; a custom drink is a clone with one quantity changed.