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:

  1. Configuration Objects: Cloning complex configuration structures with default values
  2. Database Models: Creating template records with common field values
  3. HTTP Request Templates: Cloning request objects with common headers and settings
  4. Test Data: Creating test fixtures by cloning base objects
  5. Cache Entries: Cloning cached objects to avoid modification of original data

Benefits of Prototype Pattern

  1. Performance: Avoids expensive initialization when creating similar objects
  2. Flexibility: Easy to create variations of complex objects
  3. Reduced Subclassing: Alternative to creating many subclasses for variations
  4. Runtime Configuration: Can add and remove prototypes at runtime
  5. Memory Efficiency: Shares common data structures when possible

Caveats

While the Prototype pattern is useful, consider these limitations:

  1. Deep vs Shallow Copy: Must carefully implement cloning to avoid shared references
  2. Circular References: Can cause issues with objects that reference each other
  3. Performance Overhead: Cloning itself can be expensive for very large objects
  4. Complexity: Adds complexity when simple object creation would suffice
  5. 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!