Concepts / Creational patterns / Prototype and registries

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)
StepFile loads per 50 goblinsAdding fire goblin editsTwo goblins share a loot list
Start100the ladder, a full lineno (rebuilt each time)
12a copied lineyes, a bug
2 and 32a registrationno (cloned)
42three lines, no code in the game loopno

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

02Used in these problems