This post started out as the second half of an article on prototype-based OO design for Gregory Brown’s Practicing Ruby. For an introduction to the prototyped view of the object world, and to follow along on an adventure in making Ruby act like a prototype-based language instead of a class-based one, you should go check out that article. Unlike the Practicing Ruby article, which is more about experimentation, this post focuses on the Prototype Pattern—a practical way to simplify your object models while still using classes.

A monster menagerie

Let’s say we’re writing a dungeon-crawl-style game in the vein of Nethack. So along with the  rooms and items that we’d expect to find in a text adventure game, there are also various semi-randomly-generated monsters who periodically confront the hero. Different types of monster have different stats, such as health, speed, and strength. They each have their own types of attack as well. We’d also like to be able to load up the list of monster types at run-time, from a user-editable file like this:

gnome:
  attack_text: hits you with a club!
  max_hit_points: 8
  strength: 5
  speed: 9
troll:
  attack_text: attacks you with a pickaxe!
  max_hit_points: 12
  strength: 10
  speed: 5
rabbit:
  attack_text: bites you with sharp, pointy teeth!
  max_hit_points: 50
  strength: 50
  speed: 50

One way to model different monster types would be like this:

class Monster
  attr_reader :health
  def initialize
    @health = max_hit_points
  end
end

class Gnome < Monster
  def name
    "gnome"
  end

  def attack_text
    "attacks you with a pickaxe"
  end

  def max_hit_points
    8
  end

  def strength
    5
  end

  def speed
    9
  end
end

g = Gnome.new
# => #<Gnome:0x00000004020130 @health=8>

Here there is a Monster base class, and a subclass for each type of monster. But this doesn’t really lend itself to dynamically loading arbitrary monster types from a file, so we look for other approaches.

Definitions and instances

We experiment with one design that uses MonsterDefinition classes to hold the static attributes of different monsters. A MonsterDefinition can be told to #spawn a Monster instance. The Monster instance has a reference back to its definition, as well as an instance-specific health meter (initialized based on the max_hit_points of the MonsterDefinition).

class MonsterDefinition
  attr_accessor :name,
                :attack_text,
                :max_hit_points,
                :strength,
                :speed

  def initialize(attributes={})
    attributes.each do |name, value|
      public_send("#{name}=", value)
    end
  end

  def spawn
    Monster.new(self)
  end
end

class Monster
  attr_reader :definition
  attr_accessor :health

  def initialize(definition)
    @definition = definition
    @health = definition.max_hit_points
  end
end

gnome_def = MonsterDefinition.new(
  name: "gnome",
  attack_text: "attacks you with a pickaxe!",
  max_hit_points: 8,
  strength: 5,
  speed: 9)

g = gnome_def.spawn
# => #<Monster:0x0000000401e268
# @definition=
# #<MonsterDefinition:0x0000000401e9e8
# @attack_text="attacks you with a pickaxe!",
# @max_hit_points=8,
# @name="gnome",
# @speed=9,
# @strength=5>,
# @health=8>

This approach seems promising. But as we reflect on it, we realize that we’re probably going to keep adding more of these definition/instance pairs of classes. RoomDefinition / Room, ItemDefinition / Item, etc. This feels like an awful lot of ceremony.

Cloning creatures

Finally, we hit upon using the Prototype Pattern. In this version, there is only one class: Monster. It has slots for both static attributes (like name, and strength), and dynamic attributes like health.

class Monster
  attr_accessor :name,
                :attack_text,
                :max_hit_points,
                :strength,
                :speed,
                :health

  def initialize(attributes={})
    attributes.each do |name, value|
      public_send("#{name}=", value)
    end
  end

  def initialize_dup(prototype)
    self.health = prototype.max_hit_points
    super
  end
end

To initialize our game’s bestiary of possible monster types, we load up the YAML-formatted monster file and initialize a Monster for each entry. Dynamic attributes like health are left blank for now. These are our prototypes.

require 'yaml'
bestiary = YAML.load_file('monsters.yml').each_with_object({}) do
  |(name, attributes), collection|
  collection[name] = Monster.new(attributes.merge(name: name))
end

When we want to set up a player encounter with a monster, we simply find the appropriate prototype monster, and duplicate it. The customized #initialize_dup method in Monster takes care of setting up an initial health meter for the cloned monster.

rabbit = bestiary['rabbit'].dup
# => #<Monster:0x00000000fbd948
# @attack_text="bites you with sharp, pointy teeth!",
# @max_hit_points=50,
# @name="rabbit",
# @speed=50,
# @strength=50>

we can easily generate random monsters:

random_foe = bestiary.values.sample.dup
# => #<Monster:0x00000000fc21f0
# @attack_text="attacks you with a pickaxe!",
# @max_hit_points=12,
# @name="troll",
# @speed=5,
# @strength=10>

This solution is both shorter and simpler than any of the others we tried.

Representing Forms

The Prototype Pattern is, in my experience, one of the more overlooked of the Gang of Four patterns. It is useful in many situations. As another example, consider a web application where administrators build form templates and then users fill out the forms. One way to model this is to populate the form builder interface FormDefinition objects, containing instances of TextFieldDefinition, CheckboxFieldDefinition, DateFieldDefinition, and so on. Then, when the form definition is complete and ready for user input, a new Form object is created, using the FormDefinition as a guide, with TextField, CheckboxField, DateField, etc. objects “inside” of it.

https://i2.wp.com/www.virtuouscode.com/wp-content/uploads/2012/12/wpid-form-classes-800.png?resize=432%2C123

If we apply the Prototype Pattern to this problem, we once again do away with the definition/instance dichotomy. Instead, building a new form simply means assembling a Form object, where all of the form fields have empty or placeholder values. (This makes it exceptionally easy to show a live preview of the form as it is being built). The form is published by turning on a flag marking it as a “master” form. Whenever a user fills out the form, they are really filling out a duplicate of the master.

https://i1.wp.com/www.virtuouscode.com/wp-content/uploads/2012/12/wpid-form-prototypes-800.png?resize=432%2C144

Observations

Design Patterns says that the Prototype Pattern is appropriate:

when a system should be should be independent of how its products are created, composed, and represented, and

  • when the classes to instantiate are specified at run-time, for example, by dynamic loading; or
  • to avoid building a class hierarchy of factories that parallels the class hierarchy of products; or
  • when instances of a class can have only one of a few different combinations of state. It may be more convenient to install a corresponding number of prototypes and clone them rather than instantiating the class manually, each time with the appropriate state.

The class-based view vs. the prototype-based view of OO represent a philosophical divide in how we model the world in software. We can learn from both points of view. But the lessons of the prototype-oriented mindset are not merely philosophical ones for users of class-based languages. The Prototype Pattern is a way to apply prototypes in a class-based system, one that can slash through complicated parallel inheritance hierarchies and provide a simple, flexible, and dynamic alternative. Questions? Comments? Have experience implementing the prototype pattern in your own applications? Pipe up in the comments! Oh, and do check out the article I wrote for Practicing Ruby. I had a blast metaprograming Ruby into behaving more like Self 🙂

Published by Avdi Grimm

11 Comments

  1. Interesting example. I have one small quip: inheritance. In prototypal languages, inherited properties are not owned by the new object, but by the prototype from which it was cloned.

    In this case, duplicating the object does not quite function like cloning a prototype; the duplicate has its own properties and does not delegate to the prototype if one doesn’t exist.

    Reply
  2. I had similar reaction as Mike given the example.

    My intuitive way of solving the given problem would have been something like: https://gist.github.com/4259375

    And I would not have thought of prototypes.

    Having some experience of ECMAscript languages and the way prototypes work there – I would feel that creating modules for different monster types and including them into Monster instances would be closer to the way I understand the concept of prototypes.

    But that would seem like overkill given the simple example. Perhaps some more complex scenario is needed before grasping the benefit.

    The practicing ruby article looks like it goes more into more depth. Will check it out when I have time.

    Reply
  3. Steve Yegge has a massive blog post about this approach: http://steve-yegge.blogspot.com/2008/10/universal-design-pattern.html

    Reply
  4. Interesting article. I’m going to try and give it a spin: Could this pattern be used in a Rails ActiveRecord model? In other words, what happens if Monster inherits from ActiveRecord::Base? (Let’s assume I don’t want to separate behaviour from persistance and go all hexagonal just now)

    Reply
  5. TL;DR. I would love to see this article split into multiple RubyTapas screencasts.

    Reply
  6. […] Modeling the World with Prototypes (Avdi’s follow-up article on his own blog) […]

    Reply
  7. Hey Avdi,

    I’m digging your blog’s new look. But the blockquote with the bulleted list in this article didn’t turn out too well. It looks good if disable text-align: right. I can’t think of way to make this one look good and keep the other blockquotes on the right. Sorry!

    Thanks for all the blogs and Tapas!

    -Mike

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *