Design patterns in life and Ruby — gain an intuitive understanding of OO design patterns by linking them with real-life examples.
The prototype pattern is the last creational pattern we will look at in the series.
Here is its definition:
The prototype pattern specifies the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
Before we dive into the details of the pattern, let me tell you a story about a pizza store.
Once upon a time, a wise young lady, Sihui, owned a minimalistic pizza store. The store only offered three types of pizza: pepperoni pizza, chicken pizza, and cheese pizza.
The code for the Pizza
class was straightforward.
class Pizza attr_reader :name, :toppings, :state def initialize(name, toppings) @name = name @toppings = toppings @state = 'raw' end def bake puts "Baking #{name} ..." state = 'baked' end end
A pizza had a name, a list of toppings, a state, and a bake
method.
The code for PizzaStore
was simple as well.
class PizzaStore def take_order(pizza_type) case pizza_type when 'pepperoni' pizza = Pizza.new('Pepperoni Pizza', ['pepperoni', 'shredded mozzarella cheese']) when 'chicken' pizza = Pizza.new('Chicken Pizza', ['chicken', 'mushroom', 'spinach']) else pizza = Pizza.new('Cheese Pizza', ['cheese']) end pizza.bake pizza end end
The PizzaStore
had a method to take orders, bake and return pizzas.
Using these two classes was easy. One could initialize a store and use it to take orders.
Unfortunately, the minimalistic approach was ahead of its time. Not much people came to the store because of its lack of varieties.
Sihui decided to hire a Chief Pizza Creative Officer, CPCO™, to come up more varieties.
The CPCO™ had a brilliant idea. He suggested the store to use a dynamic pizza menu and offer pizzas based on trends and seasons. For example, if matcha was the new trend, the store would have Matcha Pizza. If it was cherry harvest season, the store would offer Cherry Pizza.
Sihui wanted to try out this dynamic menu approach. But the current implementation of PizzaStore
didn’t support a dynamic menu. For example, if someone wanted to add Matcha Pizza to the menu, they would have to open the PizzaStore
class and change its code.
class PizzaStore def take_order(pizza_type) case pizza_type when 'matcha' pizza = Pizza.new('Matcha Pizza', ['matcha powder']) when 'pepperoni' pizza = Pizza.new('Pepperoni Pizza', ['pepperoni', 'shredded mozzarella cheese']) when 'chicken' pizza = Pizza.new('Chicken Pizza', ['chicken', 'mushroom', 'spinach']) else pizza = Pizza.new('Cheese Pizza', ['cheese']) end pizza.bake pizza end end
To support a dynamic menu, Sihui made some changes to the PizzaStore
.
class PizzaStore attr_accessor :pizza_prototype_collection def initialize @pizza_prototype_collection = {} end def take_order(pizza_type) pizza_prototype = pizza_prototype_collection[pizza_type] raise 'unsupported pizza type' unless pizza_prototype pizza = pizza_prototype.clone pizza.bake pizza end end
The new PizzaStore
contains a pizza_prototype_collection
. The new take_order
method, instead of creating new pizzas directly, would grab the corresponding pizza prototype and clone it to create a new pizza. (Clone is a method in Ruby that produces a shallow copy of an object. Many programming languages provide similar methods.)
With these two changes, the new PizzaStore
supported a dynamic menu. To add a new type of pizza to the menu, one could create a pizza prototype and add it to the prototype collection at run-time. As long as the corresponding prototype could be found, the PizzaStore
would be able to take order for any pizza.
Thanks to the dynamic menu, the pizza store became popular. Since then, there’re always long lines outside of the store with customers waiting to try out new pizzas from the menu. ???
Revisit The Definition of The Prototype Pattern
Woohoo, that’s the pizza story, which is also an example of the prototype pattern.
The prototype pattern specifies the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
In our example, the dynamic menu
is a collection of pizza prototypes and the take_order
method creates new pizzas by cloning the prototypes.
The Key of The Prototype Pattern
Like other creational patterns, the prototype pattern separates the creation of objects from clients that use the objects.
The key of the pattern is that each prototype is responsible for knowing how to create a corresponding object.
The agreement between prototypes and clients is that all prototypes provide a clone method and clients use it to create corresponding objects.
Advantages of The Prototype Pattern
Because the client code has no knowledge about object creation, we can:
1. at run-time, specify which object to be created
Without touching the code for PizzaStore
or restarting the program, we can easily create a new version of Matcha Pizza and update the pizza_prototype_collection
. Then ordering matcha pizza will return the Advanced Matcha Pizza, instead of the old one.
2. instantiate dynamically loaded classes
We can also dynamically load a new class Croissant
. Without having to modify the code or restarting the program, the PizzaStore
is still able to handle order for the new loaded class.
Enjoyed the article?
My best content on Software Design, Rails, and Career in Dev. Delivered weekly.
Great work and way of explaining Sihui!