Prototype

Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.

Participants

RoleIn this exampleResponsibility
PrototypeEnemyKnows how to copy itself. Ruby supplies
Prototype RegistryBestiaryHolds named, ready-made enemies and hands out copies.
ClientCaller ofAsks for a copy by name, without knowing how the original was set up.

Ruby implementation

prototype.rb
# 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
end

In 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#dup and Object#clone are built in. clone also copies frozen state and singleton methods; dup does not.
  • initialize_copy is the hook. The default copy is shallow: the clone would share the original’s @effects array, so slowing one orc would slow them all. Override initialize_copy to 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.