What is Prototype Pattern?
The Prototype pattern is a creational design pattern that allows you to create new objects by cloning existing instances rather than creating them from scratch. It’s particularly useful when object creation is expensive or complex, and you want to avoid the overhead of initializing objects repeatedly. Think of it like having a template or blueprint that you can copy and modify as needed.
I’ll demonstrate how this pattern can significantly improve performance and simplify object creation in Go applications.
Let’s start with a scenario: Game Character Creation
Imagine you’re building a game where players can create characters with various attributes, equipment, and skills. Creating a character from scratch involves loading textures, calculating stats, initializing inventory, and setting up complex relationships. Instead of doing this every time, you can create prototype characters and clone them.
Without Prototype Pattern
Here’s how you might handle character creation without the Prototype pattern:
type Character struct {
Name string
Level int
Health int
Mana int
Equipment map[string]string
Skills []string
Inventory []string
}
func CreateWarrior(name string) *Character {
// Expensive operations: loading data, calculating stats, etc.
equipment := make(map[string]string)
equipment["weapon"] = "Iron Sword"
equipment["armor"] = "Chain Mail"
equipment["shield"] = "Wooden Shield"
skills := []string{"Slash", "Block", "Charge"}
inventory := []string{"Health Potion", "Bread", "Gold Coins"}
return &Character{
Name: name,
Level: 1,
Health: 100,
Mana: 20,
Equipment: equipment,
Skills: skills,
Inventory: inventory,
}
}
func CreateMage(name string) *Character {
// Similar expensive operations for mage
equipment := make(map[string]string)
equipment["weapon"] = "Magic Staff"
equipment["armor"] = "Robe"
equipment["accessory"] = "Magic Ring"
skills := []string{"Fireball", "Heal", "Teleport"}
inventory := []string{"Mana Potion", "Spell Scroll", "Magic Crystals"}
return &Character{
Name: name,
Level: 1,
Health: 60,
Mana: 100,
Equipment: equipment,
Skills: skills,
Inventory: inventory,
}
}
// You'd need separate functions for each character type
With Prototype Pattern
Let’s refactor this using the Prototype pattern:
package main
import (
"fmt"
"encoding/json"
)
// Cloneable interface
type Cloneable interface {
Clone() Cloneable
}
// Character struct implementing Cloneable
type Character struct {
Name string `json:"name"`
Level int `json:"level"`
Health int `json:"health"`
Mana int `json:"mana"`
Equipment map[string]string `json:"equipment"`
Skills []string `json:"skills"`
Inventory []string `json:"inventory"`
}
// Deep clone implementation
func (c *Character) Clone() Cloneable {
// Create deep copy using JSON marshaling/unmarshaling
data, _ := json.Marshal(c)
var cloned Character
json.Unmarshal(data, &cloned)
return &cloned
}
// Alternative manual deep clone for better performance
func (c *Character) CloneManual() *Character {
// Clone equipment map
equipment := make(map[string]string)
for k, v := range c.Equipment {
equipment[k] = v
}
// Clone skills slice
skills := make([]string, len(c.Skills))
copy(skills, c.Skills)
// Clone inventory slice
inventory := make([]string, len(c.Inventory))
copy(inventory, c.Inventory)
return &Character{
Name: c.Name,
Level: c.Level,
Health: c.Health,
Mana: c.Mana,
Equipment: equipment,
Skills: skills,
Inventory: inventory,
}
}
// Character factory with prototypes
type CharacterFactory struct {
prototypes map[string]*Character
}
func NewCharacterFactory() *CharacterFactory {
factory := &CharacterFactory{
prototypes: make(map[string]*Character),
}
// Initialize prototypes
factory.initializePrototypes()
return factory
}
func (f *CharacterFactory) initializePrototypes() {
// Warrior prototype
warrior := &Character{
Name: "Warrior",
Level: 1,
Health: 100,
Mana: 20,
Equipment: map[string]string{
"weapon": "Iron Sword",
"armor": "Chain Mail",
"shield": "Wooden Shield",
},
Skills: []string{"Slash", "Block", "Charge"},
Inventory: []string{"Health Potion", "Bread", "Gold Coins"},
}
// Mage prototype
mage := &Character{
Name: "Mage",
Level: 1,
Health: 60,
Mana: 100,
Equipment: map[string]string{
"weapon": "Magic Staff",
"armor": "Robe",
"accessory": "Magic Ring",
},
Skills: []string{"Fireball", "Heal", "Teleport"},
Inventory: []string{"Mana Potion", "Spell Scroll", "Magic Crystals"},
}
// Archer prototype
archer := &Character{
Name: "Archer",
Level: 1,
Health: 80,
Mana: 40,
Equipment: map[string]string{
"weapon": "Longbow",
"armor": "Leather Armor",
"quiver": "Arrow Quiver",
},
Skills: []string{"Precise Shot", "Dodge", "Multi-Shot"},
Inventory: []string{"Arrows", "Rope", "Camping Kit"},
}
f.prototypes["warrior"] = warrior
f.prototypes["mage"] = mage
f.prototypes["archer"] = archer
}
func (f *CharacterFactory) CreateCharacter(characterType, name string) *Character {
prototype, exists := f.prototypes[characterType]
if !exists {
return nil
}
// Clone the prototype and customize
cloned := prototype.CloneManual()
cloned.Name = name
return cloned
}
func main() {
factory := NewCharacterFactory()
// Create multiple characters quickly
player1 := factory.CreateCharacter("warrior", "Conan")
player2 := factory.CreateCharacter("mage", "Gandalf")
player3 := factory.CreateCharacter("archer", "Legolas")
fmt.Printf("Created %s: Health=%d, Mana=%d\n",
player1.Name, player1.Health, player1.Mana)
fmt.Printf("Created %s: Health=%d, Mana=%d\n",
player2.Name, player2.Health, player2.Mana)
fmt.Printf("Created %s: Health=%d, Mana=%d\n",
player3.Name, player3.Health, player3.Mana)
// Modify one character without affecting the prototype
player1.Level = 5
player1.Health = 150
// Create another warrior - still has original stats
player4 := factory.CreateCharacter("warrior", "Barbarian")
fmt.Printf("New warrior %s: Level=%d, Health=%d\n",
player4.Name, player4.Level, player4.Health)
}
Advanced Prototype with Registry
For more complex scenarios, you can implement a prototype registry:
package main
import (
"fmt"
"sync"
)
type PrototypeRegistry struct {
prototypes map[string]Cloneable
mu sync.RWMutex
}
func NewPrototypeRegistry() *PrototypeRegistry {
return &PrototypeRegistry{
prototypes: make(map[string]Cloneable),
}
}
func (r *PrototypeRegistry) Register(name string, prototype Cloneable) {
r.mu.Lock()
defer r.mu.Unlock()
r.prototypes[name] = prototype
}
func (r *PrototypeRegistry) Create(name string) Cloneable {
r.mu.RLock()
defer r.mu.RUnlock()
if prototype, exists := r.prototypes[name]; exists {
return prototype.Clone()
}
return nil
}
func (r *PrototypeRegistry) List() []string {
r.mu.RLock()
defer r.mu.RUnlock()
var names []string
for name := range r.prototypes {
names = append(names, name)
}
return names
}
// Global registry
var Registry = NewPrototypeRegistry()
func init() {
// Register default prototypes
warrior := &Character{
Name: "Warrior",
Health: 100,
Mana: 20,
Equipment: map[string]string{
"weapon": "Iron Sword",
"armor": "Chain Mail",
},
Skills: []string{"Slash", "Block"},
}
Registry.Register("warrior", warrior)
}
func main() {
// Create from registry
newWarrior := Registry.Create("warrior").(*Character)
newWarrior.Name = "Hero"
fmt.Printf("Available prototypes: %v\n", Registry.List())
fmt.Printf("Created: %s with %d health\n", newWarrior.Name, newWarrior.Health)
}
Real-world Use Cases
Here’s where I commonly use the Prototype pattern in Go:
- Configuration Objects: Cloning complex configuration structures with default values
- Database Models: Creating template records with common field values
- HTTP Request Templates: Cloning request objects with common headers and settings
- Test Data: Creating test fixtures by cloning base objects
- Cache Entries: Cloning cached objects to avoid modification of original data
Benefits of Prototype Pattern
- Performance: Avoids expensive initialization when creating similar objects
- Flexibility: Easy to create variations of complex objects
- Reduced Subclassing: Alternative to creating many subclasses for variations
- Runtime Configuration: Can add and remove prototypes at runtime
- Memory Efficiency: Shares common data structures when possible
Caveats
While the Prototype pattern is useful, consider these limitations:
- Deep vs Shallow Copy: Must carefully implement cloning to avoid shared references
- Circular References: Can cause issues with objects that reference each other
- Performance Overhead: Cloning itself can be expensive for very large objects
- Complexity: Adds complexity when simple object creation would suffice
- Interface Pollution: Requires all cloneable objects to implement clone methods
Thank you
Thank you for reading! The Prototype pattern is excellent for scenarios where object creation is expensive or when you need many similar objects with slight variations. It’s particularly powerful in Go when combined with interfaces and proper memory management. Please drop an email at [email protected] if you would like to share any feedback or suggestions. Peace!