Prototype
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
Participants
| Role | In this example | Responsibility |
|---|---|---|
| Prototype | Enemy | Knows how to copy itself. Ruby supplies |
| Prototype Registry | Bestiary | Holds named, ready-made enemies and hands out copies. |
| Client | Caller of | Asks for a copy by name, without knowing how the original was set up. |
Ruby implementation
# The Prototype. Ruby gives every object #dup and #clone; the class only
# has to say how deep the copy should go.
class Enemy
attr_reader :name, :hp, :effects
def initialize(name, hp:, effects: [])
@name = name
@hp = hp
@effects = effects
end
def take_damage(amount)
@hp -= amount
self
end
def add_effect(effect)
@effects << effect
self
end
private
# Ruby calls this hook on the new copy after #dup or #clone. By default
# the copy shares @effects with the original, so give it its own array.
def initialize_copy(*)
super
@effects = @effects.dup
end
end
# A registry of ready-made enemies. Set up an "armoured orc" once,
# then copy it every time one needs to spawn.
class Bestiary
def initialize
@prototypes = {}
end
def register(key, enemy)
@prototypes[key] = enemy
self
end
def spawn(key)
@prototypes.fetch(key).dup
end
endIn use
bestiary = Bestiary.new
bestiary.register(:armoured_orc, Enemy.new("Orc", hp: 90, effects: [:armoured]))
orc = bestiary.spawn(:armoured_orc)
orc.take_damage(30).add_effect(:slowed)
orc.hp # => 60
orc.effects # => [:armoured, :slowed]
bestiary.spawn(:armoured_orc).effects
# => [:armoured] (the prototype is untouched)
Notes
Ruby idioms
- Every object is already a prototype.
Object#dupandObject#cloneare built in.clonealso copies frozen state and singleton methods;dupdoes not. initialize_copyis the hook. The default copy is shallow: the clone would share the original’s@effectsarray, so slowing one orc would slow them all. Overrideinitialize_copyto copy whatever must not be shared.Marshal.load(Marshal.dump(obj))gives a full deep copy, but it’s slow and fails on procs and IO. Spell the copy out instead.
Reach for it when
- Setting an object up is expensive or fiddly, and you need many copies or slight variations.
- The set of “kinds” is decided at runtime. Register a new prototype instead of writing a new class.
Watch for
- Shallow copies that share mutable state. That’s the bug the tests for this pattern guard against.
Copy first, then change the copy. The original stays as it was.