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 |