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
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
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
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
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.
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.
Observations
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 🙂
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.
Mike, this article is about the Prototype Pattern from the GoF, which is not intended to have Self/JavaScript-style inheritance. For some musings on prototype-language-style inheritance in Ruby, see the Practicing Ruby article.
Thanks for reading!
Cool, thanks Avdi. I figured the pattern wasn’t attempting to cover this case.
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.
David, this article is about the Prototype Pattern from GoF, not about prototypes as used in JavaScript, Self, etc. For the latter, see the Practicing Ruby article 🙂
Steve Yegge has a massive blog post about this approach: http://steve-yegge.blogspot.com/2008/10/universal-design-pattern.html
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)
Yeah, you can totally use it with AR.
TL;DR. I would love to see this article split into multiple RubyTapas screencasts.
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