CrackedRuby CrackedRuby

Overview

The Prototype Pattern addresses object creation by copying existing instances rather than instantiating new objects through constructors. This approach becomes valuable when object initialization involves expensive operations such as database queries, complex calculations, or resource-intensive setup procedures. Instead of repeating these operations for each new instance, the pattern creates duplicates from pre-configured prototypes.

The pattern originated from the Gang of Four design patterns catalog, where it was classified as a creational pattern alongside Factory Method, Abstract Factory, Builder, and Singleton. While these patterns share the goal of abstracting object creation, Prototype distinguishes itself through its cloning mechanism rather than explicit construction.

At its core, the pattern defines a prototype interface declaring a clone method. Concrete classes implement this interface, providing their own cloning logic. Client code requests new instances by calling clone on a prototype object, receiving a duplicate that can be modified independently from the original. This separation between creation and configuration allows systems to define object templates that serve as blueprints for new instances.

The pattern finds particular application in scenarios where classes are determined at runtime, configurations are loaded from external sources, or object graphs contain circular references that complicate traditional construction. Game development frequently uses prototypes to spawn enemies with predefined attributes. Configuration management systems clone baseline settings to create environment-specific variants. Document editors duplicate complex graphical elements without reconstructing their internal state.

# Basic prototype structure
class Document
  attr_accessor :content, :formatting, :metadata
  
  def initialize
    @content = ""
    @formatting = []
    @metadata = {}
  end
  
  def clone
    copy = self.class.new
    copy.content = @content.dup
    copy.formatting = @formatting.dup
    copy.metadata = @metadata.dup
    copy
  end
end

# Usage
template = Document.new
template.metadata[:author] = "System"
template.formatting << { type: :bold, range: 0..10 }

# Clone creates independent copy
doc1 = template.clone
doc1.content = "Report A"
doc1.metadata[:title] = "First Report"

doc2 = template.clone
doc2.content = "Report B"
# => doc1 and doc2 share template's author but have independent titles

The pattern's value emerges from its ability to decouple code that uses objects from the specifics of their construction. When a system needs 100 configured database connections, cloning a prototype with established parameters proves more efficient than repeatedly parsing configuration files and validating connection strings. The pattern also handles polymorphism naturally—client code clones objects through their abstract interface without knowing concrete types.

Key Principles

The Prototype Pattern operates on the principle that objects can serve as templates for creating new instances. The fundamental mechanism involves copying an existing object's state to a new object, bypassing the normal initialization process. This copying must handle two critical aspects: maintaining the integrity of the original object while creating an independent duplicate.

Cloning Contract: The pattern establishes a contract where objects know how to copy themselves. This differs from external copying mechanisms like serialization or reflection-based duplication. Objects encapsulate their cloning logic, ensuring that private state, invariants, and complex relationships are preserved correctly. The clone method returns a new instance with duplicated state, though the depth of this duplication requires careful consideration.

Shallow vs Deep Copying: Copying introduces a fundamental dichotomy. Shallow copying duplicates the object structure but shares references to nested objects. The cloned object references the same internal objects as the original. Deep copying recursively duplicates all nested objects, creating a completely independent object graph. The choice between these approaches depends on whether shared state is acceptable or whether complete independence is required.

class ShallowExample
  attr_accessor :name, :tags
  
  def initialize(name)
    @name = name
    @tags = []
  end
  
  def shallow_clone
    copy = self.class.new(@name)
    copy.tags = @tags  # Shares reference
    copy
  end
end

original = ShallowExample.new("Document")
original.tags << "draft"

copy = original.shallow_clone
copy.tags << "reviewed"

# Both objects share the same tags array
original.tags  # => ["draft", "reviewed"]
copy.tags      # => ["draft", "reviewed"]

Prototype Registry: Complex systems often maintain a registry of prototype objects, essentially a catalog of templates available for cloning. The registry maps identifiers to prototype instances, allowing client code to request clones by name rather than maintaining references to prototype objects. This pattern-within-a-pattern provides centralized prototype management and supports dynamic prototype registration.

class PrototypeRegistry
  def initialize
    @prototypes = {}
  end
  
  def register(key, prototype)
    @prototypes[key] = prototype
  end
  
  def create(key)
    prototype = @prototypes[key]
    raise "Unknown prototype: #{key}" unless prototype
    prototype.clone
  end
end

Initialization Separation: The pattern separates object construction from initialization. Construction refers to memory allocation and basic structure setup, handled by the language runtime. Initialization encompasses setting properties, establishing relationships, and performing validation. Traditional constructors intertwine these concerns. The Prototype Pattern performs construction through cloning, then allows targeted initialization of specific properties on the clone.

Identity vs Equality: Cloning raises questions about object identity. The clone is a distinct object with its own identity, even when its state equals the original. Ruby's object_id differs between original and clone. Equality comparisons depend on how classes define the == operator. Default equality checks object identity, which would make clones unequal. Custom equality based on state would consider clones equal if their attributes match.

Polymorphic Creation: The pattern enables polymorphic object creation where client code operates through abstract interfaces. A method accepting a prototype parameter can clone any object implementing the cloning contract, without knowing the concrete class. This flexibility supports scenarios where object types are determined at runtime or configured externally.

Ruby Implementation

Ruby provides built-in support for the Prototype Pattern through the clone and dup methods available on all objects. These methods implement shallow copying by default, creating new objects that share references to nested objects with the original. Understanding Ruby's cloning mechanisms and their implications is essential for effective prototype implementation.

Clone vs Dup: Ruby distinguishes between clone and dup in subtle ways. The clone method copies both the object's state and its frozen status. If the original object is frozen, the clone will also be frozen. The dup method copies state but does not preserve frozen status. Additionally, clone copies singleton methods defined on the specific object instance, while dup does not.

original = "Document"
original.freeze

cloned = original.clone
cloned.frozen?  # => true

duped = original.dup
duped.frozen?  # => false

# Singleton methods
def original.special_method
  "special"
end

cloned.respond_to?(:special_method)  # => true
duped.respond_to?(:special_method)   # => false

Custom Cloning: Most non-trivial classes require custom cloning logic to handle their specific needs. Ruby provides the initialize_copy method, called automatically during clone and dup operations. This method receives the original object as a parameter, allowing custom copying logic to execute after the shallow copy.

class DatabaseConnection
  attr_reader :host, :query_cache, :connected_at
  
  def initialize(host)
    @host = host
    @query_cache = []
    @connected_at = Time.now
  end
  
  def initialize_copy(original)
    super
    # Create independent cache for the clone
    @query_cache = original.query_cache.dup
    # Reset connection timestamp
    @connected_at = Time.now
  end
end

conn1 = DatabaseConnection.new("db.example.com")
conn1.query_cache << "SELECT * FROM users"

conn2 = conn1.dup
conn2.query_cache << "SELECT * FROM posts"

conn1.query_cache.size  # => 1
conn2.query_cache.size  # => 2

Deep Copying: Implementing true deep copying requires recursively cloning nested objects. Ruby does not provide automatic deep copying. Manual implementation must traverse the object graph, cloning each nested object. This becomes complex with circular references or when some objects should remain shared.

class DeepCopyable
  attr_accessor :name, :nested
  
  def initialize(name)
    @name = name
    @nested = nil
  end
  
  def initialize_copy(original)
    super
    @nested = deep_clone(@nested)
  end
  
  private
  
  def deep_clone(obj)
    case obj
    when NilClass, TrueClass, FalseClass, Numeric, Symbol
      obj  # Immutable objects can be shared
    when String
      obj.dup
    when Array
      obj.map { |item| deep_clone(item) }
    when Hash
      obj.transform_values { |value| deep_clone(value) }
    when DeepCopyable
      obj.dup  # Triggers recursive copying
    else
      obj.dup
    end
  end
end

parent = DeepCopyable.new("parent")
parent.nested = DeepCopyable.new("child")

copy = parent.dup
copy.nested.name = "modified"

parent.nested.name  # => "child" (unchanged)

Marshaling Technique: For classes without complex requirements like database connections or file handles, Ruby's Marshal module provides deep copying through serialization. Marshal.dump serializes the object graph, and Marshal.load reconstructs it, creating deep copies of all serializable objects.

class Configuration
  attr_accessor :settings, :nested_config
  
  def initialize
    @settings = {}
    @nested_config = nil
  end
  
  def deep_clone
    Marshal.load(Marshal.dump(self))
  end
end

config = Configuration.new
config.settings[:timeout] = 30
config.nested_config = Configuration.new
config.nested_config.settings[:retries] = 3

cloned = config.deep_clone
cloned.nested_config.settings[:retries] = 5

config.nested_config.settings[:retries]  # => 3 (unchanged)

Handling Uncloneable Objects: Some objects cannot or should not be cloned—database connections, file handles, threads. Custom cloning logic must handle these cases by either reestablishing connections, raising errors, or leaving references nil.

class ServiceClient
  attr_accessor :config, :connection
  
  def initialize(config)
    @config = config
    @connection = establish_connection
  end
  
  def initialize_copy(original)
    super
    # Deep copy configuration
    @config = Marshal.load(Marshal.dump(original.config))
    # Establish new connection instead of copying
    @connection = establish_connection
  end
  
  private
  
  def establish_connection
    # Expensive operation that shouldn't be shared
    OpenStruct.new(socket: rand(10000))
  end
end

Module-Based Cloning: Ruby's module system allows shared cloning behavior across multiple classes. A Cloneable module can provide generic deep cloning logic that classes mix in and customize through hooks.

module Cloneable
  def initialize_copy(original)
    super
    instance_variables.each do |var|
      value = instance_variable_get(var)
      next if unclonable?(value)
      cloned_value = clone_value(value)
      instance_variable_set(var, cloned_value)
    end
  end
  
  private
  
  def unclonable?(value)
    value.is_a?(Numeric) || value.is_a?(Symbol) || 
    value.is_a?(TrueClass) || value.is_a?(FalseClass) ||
    value.nil?
  end
  
  def clone_value(value)
    case value
    when String then value.dup
    when Array then value.map { |item| clone_value(item) }
    when Hash then value.transform_values { |v| clone_value(v) }
    else value.dup
    end
  end
end

class CacheEntry
  include Cloneable
  
  attr_accessor :key, :value, :metadata
  
  def initialize(key, value)
    @key = key
    @value = value
    @metadata = {}
  end
end

Practical Examples

The Prototype Pattern applies to diverse scenarios where object initialization complexity or cost justifies cloning pre-configured instances. These examples demonstrate the pattern's practical value across different domains.

Configuration Management: Systems often require multiple configuration objects derived from a base template. Rather than parsing configuration files repeatedly or duplicating setup logic, cloning a prototype configuration provides consistent initialization with customizable overrides.

class ApplicationConfig
  attr_accessor :database, :cache, :logging, :features
  
  def initialize
    @database = { host: nil, port: nil, pool_size: 5 }
    @cache = { enabled: true, ttl: 3600 }
    @logging = { level: :info, output: :stdout }
    @features = {}
  end
  
  def initialize_copy(original)
    super
    @database = original.database.dup
    @cache = original.cache.dup
    @logging = original.logging.dup
    @features = original.features.dup
  end
end

# Base configuration
base_config = ApplicationConfig.new
base_config.database[:pool_size] = 10
base_config.features[:new_ui] = true

# Development configuration
dev_config = base_config.dup
dev_config.database[:host] = "localhost"
dev_config.logging[:level] = :debug

# Production configuration
prod_config = base_config.dup
prod_config.database[:host] = "prod.example.com"
prod_config.cache[:ttl] = 7200

# Each configuration shares base settings but has independent overrides

Game Entity Spawning: Game development frequently requires creating multiple entities with similar attributes. Enemy types, particle effects, and UI elements benefit from prototype-based spawning, where a prototype defines default behavior and statistics that individual instances can modify.

class Enemy
  attr_accessor :health, :damage, :speed, :abilities, :position
  
  def initialize
    @health = 100
    @damage = 10
    @speed = 5
    @abilities = []
    @position = { x: 0, y: 0 }
  end
  
  def initialize_copy(original)
    super
    @abilities = original.abilities.dup
    @position = original.position.dup
  end
  
  def spawn_at(x, y)
    clone.tap do |enemy|
      enemy.position = { x: x, y: y }
    end
  end
end

# Define enemy types
goblin_prototype = Enemy.new
goblin_prototype.health = 50
goblin_prototype.damage = 8
goblin_prototype.abilities = [:melee_attack]

orc_prototype = Enemy.new
orc_prototype.health = 150
orc_prototype.damage = 20
orc_prototype.abilities = [:melee_attack, :rage]

# Spawn instances
wave1 = [
  goblin_prototype.spawn_at(10, 20),
  goblin_prototype.spawn_at(15, 25),
  orc_prototype.spawn_at(30, 40)
]

# Each enemy has independent position and can be modified
wave1[0].health -= 10  # Doesn't affect prototype or other goblins

Document Templates: Document processing systems use prototypes to create documents from templates. A template document contains formatting, styles, and boilerplate content. Cloning the template produces new documents that inherit these attributes while allowing content customization.

class StyledDocument
  attr_accessor :title, :sections, :styles, :metadata
  
  def initialize
    @title = ""
    @sections = []
    @styles = {}
    @metadata = {}
  end
  
  def initialize_copy(original)
    super
    @sections = original.sections.map(&:dup)
    @styles = original.styles.transform_values(&:dup)
    @metadata = original.metadata.dup
  end
end

class DocumentSection
  attr_accessor :heading, :content, :level
  
  def initialize(heading, level = 1)
    @heading = heading
    @content = ""
    @level = level
  end
end

# Create report template
report_template = StyledDocument.new
report_template.styles[:heading1] = { font: "Arial", size: 24, bold: true }
report_template.styles[:body] = { font: "Arial", size: 12 }
report_template.metadata[:company] = "TechCorp"

intro = DocumentSection.new("Introduction", 1)
body = DocumentSection.new("Analysis", 1)
conclusion = DocumentSection.new("Conclusion", 1)
report_template.sections = [intro, body, conclusion]

# Generate specific reports
q1_report = report_template.dup
q1_report.title = "Q1 Performance Report"
q1_report.sections[1].content = "Q1 showed 15% growth..."

q2_report = report_template.dup
q2_report.title = "Q2 Performance Report"
q2_report.sections[1].content = "Q2 maintained momentum..."

Database Query Builders: Query builders construct complex database queries through method chaining. Cloning partially-built queries allows reusing common query patterns while specializing for specific cases.

class QueryBuilder
  attr_accessor :select_clause, :from_clause, :where_conditions, :joins
  
  def initialize
    @select_clause = []
    @from_clause = nil
    @where_conditions = []
    @joins = []
  end
  
  def initialize_copy(original)
    super
    @select_clause = original.select_clause.dup
    @where_conditions = original.where_conditions.dup
    @joins = original.joins.dup
  end
  
  def select(*fields)
    @select_clause.concat(fields)
    self
  end
  
  def from(table)
    @from_clause = table
    self
  end
  
  def where(condition)
    @where_conditions << condition
    self
  end
  
  def join(table, on)
    @joins << { table: table, on: on }
    self
  end
  
  def to_sql
    parts = ["SELECT #{@select_clause.join(', ')}"]
    parts << "FROM #{@from_clause}" if @from_clause
    @joins.each { |j| parts << "JOIN #{j[:table]} ON #{j[:on]}" }
    parts << "WHERE #{@where_conditions.join(' AND ')}" if @where_conditions.any?
    parts.join(' ')
  end
end

# Base query for user analytics
base_query = QueryBuilder.new
  .select('users.id', 'users.email')
  .from('users')
  .join('subscriptions', 'users.id = subscriptions.user_id')

# Clone for active users
active_query = base_query.dup
  .where('users.status = "active"')
  .where('subscriptions.expires_at > NOW()')

# Clone for trial users  
trial_query = base_query.dup
  .where('subscriptions.type = "trial"')
  .select('subscriptions.started_at')

# Each query independently extends the base pattern

Design Considerations

The Prototype Pattern addresses specific design challenges but introduces its own trade-offs. Understanding when to apply the pattern and how it compares to alternatives guides appropriate usage.

When to Use Prototypes: The pattern provides value when object construction involves significant cost or complexity. Database connections requiring authentication handshakes, objects loading data from external sources, or instances with intricate nested structures benefit from cloning pre-initialized prototypes. If instantiation is trivial—a few property assignments—traditional constructors prove simpler and more direct.

The pattern also suits scenarios where classes are determined at runtime. A plugin system loading class definitions dynamically can register prototype instances rather than class references. Client code clones prototypes through a common interface without compile-time knowledge of concrete types. This flexibility supports extensible architectures where new types are added without modifying existing code.

Systems with numerous similar object configurations gain from prototypes. Consider a test suite requiring 50 test objects with slightly varying properties. Defining a prototype and cloning it with targeted modifications proves more maintainable than 50 explicit instantiations. The prototype serves as documentation of the default configuration, making variations explicit.

Comparison with Factory Patterns: Factory Method and Abstract Factory also abstract object creation, but through different mechanisms. Factories encapsulate construction logic in factory classes or methods. Client code calls factory methods, which instantiate and configure objects. Prototypes shift this responsibility to the objects themselves through cloning.

Factories excel when construction logic requires external resources, complex validation, or coordination between multiple objects. A database connection factory might check connection pools, validate credentials, and handle failures. This coordination logic fits naturally in a factory class but would clutter prototype cloning methods.

Prototypes excel when configuration complexity outweighs construction logic. An object with 20 configurable properties but simple construction benefits from prototype cloning. Configure the prototype once, then clone and modify specific properties. Factories would require passing numerous parameters or builder methods, introducing more complexity than cloning.

# Factory approach
class ConnectionFactory
  def create(type, host, port, options = {})
    case type
    when :postgresql
      PostgresConnection.new(host, port, options)
    when :mysql
      MySQLConnection.new(host, port, options)
    end
  end
end

# Prototype approach
postgres_prototype = PostgresConnection.new("localhost", 5432, { pool: 10 })
mysql_prototype = MySQLConnection.new("localhost", 3306, { pool: 10 })

# Clone prototypes for specific databases
users_db = postgres_prototype.dup.tap { |c| c.database = "users" }
orders_db = postgres_prototype.dup.tap { |c| c.database = "orders" }

Shallow vs Deep Copying Trade-offs: Shallow copying creates new objects sharing references to nested objects. This approach is fast and memory-efficient but introduces aliasing where modifications to nested objects affect multiple instances. Deep copying eliminates aliasing by recursively duplicating nested objects, but requires more memory and processing time.

The choice depends on whether shared state is acceptable. Immutable nested objects can be safely shared—strings, numbers, frozen collections. Mutable nested objects require deep copying if independence is required. Some scenarios intentionally share state. Multiple document objects might reference a common style registry, where modifications should propagate to all documents.

Performance considerations also factor in. Deep copying large object graphs becomes expensive. If most clones never modify nested objects, deep copying wastes resources. Lazy copying strategies can defer deep copying until modification occurs, though this adds complexity.

Memory and Performance Trade-offs: The pattern trades memory for initialization speed. Maintaining prototype instances consumes memory even when no clones exist. Systems with hundreds of prototype types must balance the memory cost against construction savings. For prototypes used infrequently, the memory cost may exceed the construction savings.

Cloning performance depends on object complexity. Shallow cloning is fast—allocating memory and copying references. Deep cloning scales with object graph size and depth. Profiling is essential to verify that cloning actually improves performance. If construction is fast and cloning is slow, the pattern provides no benefit.

Comparison with Object Pools: Object pools maintain collections of reusable objects. Rather than creating and destroying objects, pools lease objects for temporary use and return them. This pattern shares the goal of avoiding construction overhead but achieves it through reuse rather than cloning.

Pools work well for objects with expensive initialization but simple state. Database connections, threads, and network sockets benefit from pooling. Objects with complex state that must be reset between uses prove difficult to pool effectively. Prototypes suit scenarios where each object needs distinct state from inception.

Thread Safety: Prototypes must consider concurrent access. If multiple threads clone the same prototype simultaneously, the clone and initialize_copy methods must be thread-safe. Shallow copying is inherently safe—no shared state is modified. Deep copying requires careful handling if prototype state is mutable. Freezing prototype objects after initialization prevents modification and ensures thread safety.

Common Patterns

The basic Prototype Pattern extends into several variations that address specific requirements. These patterns build on the core cloning mechanism while adding structure for complex scenarios.

Prototype Registry Pattern: A registry maintains a collection of prototype objects indexed by keys. Client code requests clones by name rather than maintaining direct references to prototypes. This pattern provides centralized prototype management and supports dynamic registration and lookup.

class PrototypeRegistry
  def initialize
    @prototypes = {}
  end
  
  def register(key, prototype)
    @prototypes[key] = prototype
    self
  end
  
  def unregister(key)
    @prototypes.delete(key)
  end
  
  def create(key, **overrides)
    prototype = @prototypes[key]
    raise ArgumentError, "Unknown prototype: #{key}" unless prototype
    
    clone = prototype.dup
    overrides.each { |attr, value| clone.send("#{attr}=", value) }
    clone
  end
  
  def list
    @prototypes.keys
  end
end

# Setup registry
registry = PrototypeRegistry.new

small_box = Package.new
small_box.dimensions = { width: 10, height: 10, depth: 10 }
small_box.weight_limit = 5

large_box = Package.new
large_box.dimensions = { width: 30, height: 30, depth: 30 }
large_box.weight_limit = 20

registry.register(:small, small_box)
registry.register(:large, large_box)

# Use registry
package1 = registry.create(:small, destination: "New York")
package2 = registry.create(:large, destination: "Los Angeles")

Prototype Manager with Lazy Initialization: Some prototypes require expensive initialization that should be deferred until first use. A prototype manager loads and caches prototypes on demand, initializing them only when requested.

class PrototypeManager
  def initialize
    @prototypes = {}
    @initializers = {}
  end
  
  def register_initializer(key, &block)
    @initializers[key] = block
    self
  end
  
  def create(key)
    ensure_prototype_loaded(key)
    @prototypes[key].dup
  end
  
  private
  
  def ensure_prototype_loaded(key)
    return if @prototypes.key?(key)
    
    initializer = @initializers[key]
    raise ArgumentError, "Unknown prototype: #{key}" unless initializer
    
    @prototypes[key] = initializer.call
  end
end

manager = PrototypeManager.new

# Register initialization logic without creating prototypes yet
manager.register_initializer(:database_config) do
  config = DatabaseConfig.new
  config.load_from_file("config/database.yml")
  config
end

manager.register_initializer(:api_client) do
  client = APIClient.new
  client.authenticate
  client.load_endpoints
  client
end

# Prototypes created only when first requested
db_config = manager.create(:database_config)  # Initializes prototype
api_client = manager.create(:api_client)      # Initializes prototype

Hierarchical Prototypes: Prototypes can form hierarchies where specialized prototypes extend more general ones. This pattern enables inheritance-like relationships without class hierarchies, useful for configuration systems or plugin architectures.

class ConfigurationPrototype
  attr_accessor :attributes, :parent
  
  def initialize(parent = nil)
    @attributes = {}
    @parent = parent
  end
  
  def get(key)
    return @attributes[key] if @attributes.key?(key)
    @parent&.get(key)
  end
  
  def set(key, value)
    @attributes[key] = value
  end
  
  def initialize_copy(original)
    super
    @attributes = original.attributes.dup
    # Parent reference is shared, not cloned
  end
end

# Base configuration
base = ConfigurationPrototype.new
base.set(:timeout, 30)
base.set(:retries, 3)

# Service-specific configuration inherits from base
api_service = ConfigurationPrototype.new(base)
api_service.set(:endpoint, "https://api.example.com")
api_service.set(:timeout, 60)  # Override

# Environment-specific configuration
dev_api = api_service.dup
dev_api.set(:endpoint, "http://localhost:3000")

dev_api.get(:endpoint)  # => "http://localhost:3000"
dev_api.get(:retries)   # => 3 (from base)

Copy-on-Write Prototypes: For objects with large internal state, copy-on-write defers deep copying until modification occurs. The clone initially shares state with the prototype, tracking shared references. Upon modification, the affected portions are deep copied, leaving unmodified portions shared.

class CopyOnWriteDocument
  attr_reader :metadata
  
  def initialize
    @content = []
    @metadata = {}
    @shared_content = true
  end
  
  def initialize_copy(original)
    super
    @shared_content = true
    # Content reference is shared initially
    @metadata = original.metadata.dup
  end
  
  def add_paragraph(text)
    unshare_content if @shared_content
    @content << text
  end
  
  def content
    @content.dup
  end
  
  private
  
  def unshare_content
    @content = @content.dup
    @shared_content = false
  end
end

Builder-Prototype Hybrid: Combining the Builder and Prototype patterns allows constructing complex objects step-by-step, then cloning the result as a template. This pattern suits scenarios where multiple similar objects require incremental construction.

class QueryPrototype
  attr_accessor :parts
  
  def initialize
    @parts = {
      select: [],
      from: nil,
      where: [],
      order: []
    }
  end
  
  def initialize_copy(original)
    super
    @parts = Marshal.load(Marshal.dump(original.parts))
  end
end

class QueryBuilder
  def initialize(prototype = nil)
    @prototype = prototype || QueryPrototype.new
  end
  
  def select(*fields)
    @prototype.parts[:select].concat(fields)
    self
  end
  
  def from(table)
    @prototype.parts[:from] = table
    self
  end
  
  def where(condition)
    @prototype.parts[:where] << condition
    self
  end
  
  def build
    @prototype
  end
  
  def clone_builder
    QueryBuilder.new(@prototype.dup)
  end
end

# Build base query
base_builder = QueryBuilder.new
  .select('id', 'name')
  .from('users')
base_prototype = base_builder.build

# Clone builder to extend query
active_users = base_builder.clone_builder
  .where('status = "active"')
  .build

Common Pitfalls

The Prototype Pattern's apparent simplicity masks several subtle issues that can cause bugs or undermine its benefits. Recognizing these pitfalls helps avoid common mistakes.

Shallow Copy Aliasing: The most frequent mistake involves shallow copying objects with mutable nested state. The clone shares references to nested objects, causing modifications to affect both original and clone unexpectedly.

class Portfolio
  attr_accessor :owner, :holdings
  
  def initialize(owner)
    @owner = owner
    @holdings = []
  end
end

# Problematic shallow copy
template = Portfolio.new("Template")
template.holdings << { stock: "AAPL", shares: 100 }

portfolio1 = template.dup
portfolio1.owner = "Alice"
portfolio1.holdings << { stock: "GOOGL", shares: 50 }

portfolio2 = template.dup
portfolio2.owner = "Bob"

# All portfolios share the same holdings array
template.holdings.size   # => 2 (unexpectedly modified)
portfolio1.holdings.size # => 2
portfolio2.holdings.size # => 2

# Fix with proper initialize_copy
class Portfolio
  def initialize_copy(original)
    super
    @holdings = original.holdings.map(&:dup)
  end
end

Forgetting to Override initialize_copy: Ruby's default clone and dup perform shallow copies. Without custom initialize_copy, classes with complex state will share references to nested objects. This pitfall manifests subtly since shallow copying works correctly for simple objects.

Cloning Singleton or Stateful Objects: Some objects should not be cloned—database connections, file handles, singletons. Cloning these objects creates invalid states or violates assumptions. Custom cloning logic must detect and handle these cases.

class Configuration
  include Singleton
  
  attr_accessor :settings, :database
  
  def initialize
    @settings = {}
    @database = DatabaseConnection.new
  end
  
  # Problematic: cloning singleton
  config1 = Configuration.instance
  config2 = config1.dup  # Creates second singleton instance
  
  # Fix: prevent cloning
  def initialize_copy(original)
    raise TypeError, "Cannot clone singleton Configuration"
  end
end

Ignoring Object Identity: Clones are distinct objects with different object_id values. Code relying on object identity rather than equality will treat clones as different even when their state is identical. Hash keys using object identity rather than value will not recognize clones as equivalent keys.

class User
  attr_accessor :id, :name
  
  def initialize(id, name)
    @id = id
    @name = name
  end
end

user1 = User.new(1, "Alice")
user2 = user1.dup

# Identity comparison fails
user1.equal?(user2)  # => false
user1.object_id == user2.object_id  # => false

# Using as hash keys
cache = {}
cache[user1] = "data"
cache[user2]  # => nil (different keys)

# Fix: define equality and hash methods
class User
  def ==(other)
    other.is_a?(User) && id == other.id
  end
  
  def eql?(other)
    self == other
  end
  
  def hash
    id.hash
  end
end

Circular Reference Issues: Deep copying objects with circular references requires special handling. Naive recursive copying enters infinite loops when encountering circular references. Tracking visited objects prevents this but adds complexity.

class Node
  attr_accessor :value, :next
  
  def initialize(value)
    @value = value
    @next = nil
  end
end

# Create circular structure
node1 = Node.new(1)
node2 = Node.new(2)
node1.next = node2
node2.next = node1

# Naive deep copy causes infinite loop
def deep_copy(node)
  return nil if node.nil?
  copy = node.dup
  copy.next = deep_copy(node.next)  # Infinite recursion
  copy
end

# Fix: track visited objects
def deep_copy_safe(node, visited = {})
  return nil if node.nil?
  return visited[node.object_id] if visited.key?(node.object_id)
  
  copy = node.dup
  visited[node.object_id] = copy
  copy.next = deep_copy_safe(node.next, visited)
  copy
end

Performance Assumptions: Assuming cloning always improves performance leads to misuse. Cloning complex object graphs may be slower than construction. Prototypes consume memory even when unused. Profiling is essential to verify performance benefits.

Prototype Modification: Modifying prototype objects after registering them causes all future clones to reflect those changes. This can be intentional but often represents a bug. Freezing prototypes prevents accidental modification.

registry = PrototypeRegistry.new

config = Configuration.new
config.timeout = 30
registry.register(:default, config)

# Later modification affects future clones
config.timeout = 60

clone1 = registry.create(:default)
clone1.timeout  # => 60 (unexpected)

# Fix: freeze prototypes
config.freeze
registry.register(:default, config)

Reference

Cloning Method Comparison

Method Copies Frozen State Copies Singleton Methods Use Case
clone Yes Yes Complete object duplication
dup No No Copying without special attributes
Marshal.load(Marshal.dump(obj)) No No Deep copying serializable objects
Custom initialize_copy Controlled Controlled Complex object cloning logic

Ruby Cloning Behavior

Object Type Default Behavior Sharing Notes
Numeric Returns self Shared Immutable, no cloning needed
Symbol Returns self Shared Immutable, singleton instances
String Shallow copy Independent Content duplicated
Array Shallow copy Shared elements Elements referenced, not copied
Hash Shallow copy Shared values Keys and values referenced
Custom objects Shallow copy Shared ivars Requires initialize_copy for deep copy

Pattern Participants

Role Responsibility Implementation
Prototype Declares cloning interface Abstract class or module
Concrete Prototype Implements cloning logic Classes with initialize_copy
Client Requests clones from prototypes Calls clone or dup
Prototype Registry Manages prototype collection Hash-based lookup

When to Use Prototypes

Scenario Use Prototypes Alternative
Expensive initialization Yes Factory with caching
Many similar configurations Yes Builder pattern
Runtime class selection Yes Factory pattern
Simple object construction No Direct instantiation
Objects requiring external resources Carefully Factory with resource management
Immutable objects No Direct instantiation

Common Implementation Checklist

Step Action Purpose
1 Identify expensive object creation Validate pattern applicability
2 Implement initialize_copy Define cloning behavior
3 Handle nested objects Prevent aliasing issues
4 Test clone independence Verify proper copying
5 Consider freezing prototypes Prevent accidental modification
6 Document cloning depth Clarify shallow vs deep copying
7 Profile performance Confirm performance benefits

Code Template

# Basic prototype implementation
class Prototype
  attr_accessor :state
  
  def initialize
    @state = {}
    @complex_object = nil
  end
  
  def initialize_copy(original)
    super
    # Deep copy mutable state
    @state = original.state.dup
    @complex_object = original.complex_object&.dup
  end
end

# Registry implementation
class PrototypeRegistry
  def initialize
    @prototypes = {}
  end
  
  def register(key, prototype)
    @prototypes[key] = prototype
  end
  
  def create(key)
    prototype = @prototypes[key]
    raise "Unknown prototype: #{key}" unless prototype
    prototype.dup
  end
end

# Usage pattern
registry = PrototypeRegistry.new
prototype = Prototype.new
prototype.state[:config] = "value"
registry.register(:default, prototype)

instance = registry.create(:default)

Deep Copy Strategies

Strategy Pros Cons When to Use
Manual recursive Full control Complex implementation Custom requirements
Marshal Simple, automatic Limited to serializable objects Standard objects
JSON serialization Human-readable Loses type information Simple data structures
Visitor pattern Extensible Requires visitor implementation Complex object graphs
Copy-on-write Memory efficient Complex tracking Large objects, infrequent modification