Overview
Constructors and destructors form the lifecycle boundaries of objects in object-oriented programming. A constructor executes when an object comes into existence, establishing its initial state and allocating required resources. A destructor executes when an object ceases to exist, releasing resources and performing cleanup operations.
The constructor concept originated in Simula 67, the first object-oriented programming language, and became formalized in C++ with explicit constructor and destructor syntax. Languages differ significantly in their implementation of these concepts. C++ and Java provide explicit constructors with method overloading, while Ruby uses the initialize method called by new. Some languages like Go lack traditional constructors entirely, relying on factory functions and composite literals.
Destructors vary even more dramatically across languages. C++ provides deterministic destructors that execute immediately when objects go out of scope, enabling the Resource Acquisition Is Initialization (RAII) pattern. Languages with garbage collection, including Ruby, Java, and Python, offer finalizers that execute non-deterministically at some point before memory reclamation. This fundamental difference affects how developers approach resource management in each language.
class FileHandler
def initialize(filename)
@file = File.open(filename, 'r')
@filename = filename
end
def close
@file.close if @file && !@file.closed?
end
end
handler = FileHandler.new('data.txt')
# File opened during initialization
handler.close
# Explicit cleanup required
The distinction between construction and initialization matters in languages with multi-phase object creation. In Ruby, new allocates memory and returns the instance, while initialize sets up the object's state. In C++, constructors both allocate and initialize. Understanding these mechanisms determines how developers structure object creation, manage resources, and handle initialization failures.
Key Principles
Object construction consists of memory allocation followed by initialization. The memory allocation phase creates storage for the object's instance variables and metadata. The initialization phase assigns values to instance variables and establishes object invariants—conditions that must remain true throughout the object's lifetime.
Constructor execution follows a defined order in languages with inheritance. Base class constructors execute before derived class constructors, building the object from the foundation upward. This ordering ensures that inherited state exists before subclass initialization code runs. Ruby calls initialize methods from the inheritance chain automatically through super, while C++ invokes base constructors implicitly before the derived constructor body executes.
class Vehicle
def initialize(wheels)
@wheels = wheels
puts "Vehicle initialized with #{wheels} wheels"
end
end
class Car < Vehicle
def initialize(make, model)
super(4) # Explicit base class initialization
@make = make
@model = model
puts "Car initialized: #{make} #{model}"
end
end
car = Car.new('Toyota', 'Camry')
# Output:
# Vehicle initialized with 4 wheels
# Car initialized: Toyota Camry
Destructors execute in reverse order from constructors. In inheritance hierarchies, derived class destructors run before base class destructors, tearing down the object from top to bottom. This ordering ensures that code in a base class destructor doesn't access members that have already been destroyed in derived classes.
Constructor parameters establish how clients create objects. Parameter lists define required initialization data and set defaults. Languages with method overloading permit multiple constructors with different parameter signatures, each representing a different way to create valid objects. Ruby achieves similar flexibility through optional parameters, keyword arguments, and default values rather than method overloading.
class Configuration
def initialize(host: 'localhost', port: 8080, timeout: 30)
@host = host
@port = port
@timeout = timeout
end
end
# Multiple valid initialization approaches
config1 = Configuration.new
config2 = Configuration.new(host: 'api.example.com')
config3 = Configuration.new(host: 'api.example.com', port: 443, timeout: 60)
Object invariants represent conditions that remain true after construction completes and before destruction begins. Constructors establish these invariants, and public methods maintain them. A well-designed constructor ensures that no sequence of public method calls can create invalid object states.
Resource management through constructors and destructors follows the principle that resource acquisition is initialization. An object acquires resources during construction and releases them during destruction. This pattern guarantees resource cleanup even when exceptions occur, provided the language supports deterministic destruction or explicit resource management protocols.
Initialization order within a single object matters when instance variables depend on each other. Ruby executes assignments in initialize sequentially, meaning early statements can't reference later instance variables. Circular dependencies in initialization indicate design problems requiring refactoring.
class DataProcessor
def initialize(source_file, output_dir)
@source_file = source_file
@output_dir = output_dir
# Can reference both variables after assignment
@output_path = File.join(@output_dir, File.basename(@source_file))
end
end
Default constructors execute when no arguments are provided. Languages like C++ generate default constructors automatically if none are defined, but Ruby requires explicit initialize methods. The absence of a default constructor forces clients to provide initialization data, preventing invalid object creation.
Copy constructors create new objects from existing objects. C++ provides explicit copy constructor syntax, while Ruby uses dup and clone methods that call initialize_copy. The distinction between shallow and deep copying affects object semantics—shallow copies share references to mutable objects, while deep copies duplicate everything recursively.
Ruby Implementation
Ruby separates object allocation from initialization through new and initialize. The new class method allocates memory, invokes initialize on the new instance, and returns the instance. Developers define initialize as a private instance method that receives arguments passed to new.
class Person
def initialize(name, age)
@name = name
@age = age
end
def greet
"Hello, I'm #{@name}, #{@age} years old"
end
end
person = Person.new('Alice', 30)
# new allocates object, calls initialize('Alice', 30), returns instance
puts person.greet
# => Hello, I'm Alice, 30 years old
Ruby constructors use instance variables (prefixed with @) to store object state. Unlike languages requiring explicit field declarations, Ruby creates instance variables on first assignment. This flexibility allows conditional instance variable creation but can obscure which variables an object contains.
class OptionalFeatures
def initialize(enable_logging: false, enable_caching: false)
@logger = Logger.new(STDOUT) if enable_logging
@cache = {} if enable_caching
end
def log(message)
@logger&.info(message) # Safe navigation operator
end
def fetch(key)
return @cache[key] if @cache&.key?(key)
# Fetch from source
end
end
Attribute readers and writers provide controlled access to instance variables. Ruby generates these methods through attr_reader, attr_writer, and attr_accessor. Defining these outside initialize establishes the object's interface while keeping initialization focused on setup logic.
class Product
attr_reader :name, :price
attr_accessor :quantity
def initialize(name, price, quantity = 0)
@name = name
@price = price
@quantity = quantity
validate!
end
private
def validate!
raise ArgumentError, 'Price must be positive' if @price <= 0
raise ArgumentError, 'Name cannot be empty' if @name.empty?
end
end
Ruby lacks explicit destructors but provides finalizers through ObjectSpace.define_finalizer. Finalizers run during garbage collection, making execution timing unpredictable. The Ruby documentation warns against relying on finalizers for critical cleanup because garbage collection may never occur before program termination.
class ResourceHolder
def initialize(resource_id)
@resource_id = resource_id
@resource = acquire_resource(resource_id)
# Define finalizer (not recommended for critical cleanup)
ObjectSpace.define_finalizer(self, self.class.method(:finalize).to_proc)
end
def self.finalize(id)
puts "Finalizer called for object #{id}"
# Cleanup code here
end
def close
release_resource(@resource)
@resource = nil
end
private
def acquire_resource(id)
# Resource acquisition logic
end
def release_resource(resource)
# Resource release logic
end
end
Ruby prefers explicit cleanup methods over finalizers. The convention names these methods close, dispose, or cleanup. Blocks and ensure clauses guarantee cleanup even when exceptions occur, providing deterministic resource management despite non-deterministic finalization.
class Database
def initialize(connection_string)
@connection = establish_connection(connection_string)
end
def close
@connection.disconnect if @connection
@connection = nil
end
def self.open(connection_string)
database = new(connection_string)
return database unless block_given?
begin
yield database
ensure
database.close
end
end
end
# Block-based usage ensures cleanup
Database.open('postgresql://localhost/mydb') do |db|
db.query('SELECT * FROM users')
end # close called automatically
Ruby's initialize_copy method executes during object duplication via dup or clone. This method controls how instance variables transfer to the new object. By default, Ruby performs shallow copies, sharing references to mutable objects. Deep copies require explicit implementation in initialize_copy.
class ShoppingCart
attr_reader :items
def initialize
@items = []
end
def add_item(item)
@items << item
end
def initialize_copy(original)
super
@items = original.items.dup # Shallow copy of array
end
end
cart1 = ShoppingCart.new
cart1.add_item('Book')
cart2 = cart1.dup
cart2.add_item('Pen')
puts cart1.items.inspect # => ["Book"]
puts cart2.items.inspect # => ["Book", "Pen"]
Singleton classes allow per-object constructors through initialize_singleton. This advanced feature creates object-specific methods during construction, supporting prototype-based programming patterns within Ruby's class-based system.
class CustomizedService
def initialize(config)
@config = config
# Define singleton methods based on configuration
if config[:enable_monitoring]
singleton_class.define_method(:monitor) do
puts "Monitoring enabled for #{self}"
end
end
end
end
service = CustomizedService.new(enable_monitoring: true)
service.monitor # Method exists due to configuration
Practical Examples
File handling demonstrates the necessity of explicit cleanup in Ruby. Opening files during construction requires closing them before the object becomes unreachable. The block-based pattern ensures cleanup regardless of exceptions.
class LogAnalyzer
def initialize(log_file_path)
@log_file_path = log_file_path
@file = File.open(log_file_path, 'r')
@line_count = 0
@error_count = 0
end
def analyze
@file.each_line do |line|
@line_count += 1
@error_count += 1 if line.include?('ERROR')
end
end
def report
{
file: @log_file_path,
total_lines: @line_count,
errors: @error_count,
error_rate: @error_count.to_f / @line_count
}
end
def close
@file.close if @file && !@file.closed?
end
def self.analyze_file(log_file_path)
analyzer = new(log_file_path)
analyzer.analyze
analyzer.report
ensure
analyzer&.close
end
end
# Safe usage pattern
report = LogAnalyzer.analyze_file('app.log')
puts "Error rate: #{(report[:error_rate] * 100).round(2)}%"
Database connection pooling illustrates resource management across multiple objects. The pool constructor initializes connections, and each connection's constructor establishes database communication. The pool's cleanup method closes all connections.
class DatabaseConnection
attr_reader :id, :created_at
def initialize(connection_string, id)
@id = id
@connection_string = connection_string
@connection = establish_connection
@created_at = Time.now
@in_use = false
end
def execute(query)
raise 'Connection in use' if @in_use
@in_use = true
begin
@connection.execute(query)
ensure
@in_use = false
end
end
def close
@connection.disconnect if @connection
@connection = nil
end
private
def establish_connection
# Actual connection logic
OpenStruct.new(execute: ->(q) { "Result: #{q}" }, disconnect: -> {})
end
end
class ConnectionPool
attr_reader :size
def initialize(connection_string, size: 5)
@connection_string = connection_string
@size = size
@connections = Array.new(size) { |i| DatabaseConnection.new(connection_string, i) }
@available = @connections.dup
@mutex = Mutex.new
end
def with_connection
connection = acquire
begin
yield connection
ensure
release(connection)
end
end
def close_all
@mutex.synchronize do
@connections.each(&:close)
@connections.clear
@available.clear
end
end
private
def acquire
@mutex.synchronize do
sleep 0.1 until @available.any?
@available.shift
end
end
def release(connection)
@mutex.synchronize do
@available << connection
end
end
end
# Usage
pool = ConnectionPool.new('postgresql://localhost/app', size: 3)
pool.with_connection do |conn|
conn.execute('SELECT * FROM users')
end
pool.close_all
Configuration objects demonstrate validation during construction. The constructor validates inputs and establishes invariants, preventing invalid object creation.
class ServerConfiguration
attr_reader :host, :port, :protocol, :timeout
VALID_PROTOCOLS = ['http', 'https'].freeze
PORT_RANGE = (1..65535).freeze
def initialize(host:, port:, protocol: 'https', timeout: 30)
@host = validate_host(host)
@port = validate_port(port)
@protocol = validate_protocol(protocol)
@timeout = validate_timeout(timeout)
@created_at = Time.now
end
def endpoint
"#{@protocol}://#{@host}:#{@port}"
end
def age
Time.now - @created_at
end
private
def validate_host(host)
raise ArgumentError, 'Host cannot be empty' if host.nil? || host.empty?
host
end
def validate_port(port)
port = port.to_i
unless PORT_RANGE.include?(port)
raise ArgumentError, "Port must be between #{PORT_RANGE.min} and #{PORT_RANGE.max}"
end
port
end
def validate_protocol(protocol)
protocol = protocol.downcase
unless VALID_PROTOCOLS.include?(protocol)
raise ArgumentError, "Protocol must be one of: #{VALID_PROTOCOLS.join(', ')}"
end
protocol
end
def validate_timeout(timeout)
timeout = timeout.to_i
raise ArgumentError, 'Timeout must be positive' if timeout <= 0
timeout
end
end
# Valid configuration
config = ServerConfiguration.new(host: 'api.example.com', port: 443)
puts config.endpoint # => https://api.example.com:443
# Invalid configuration raises exception
begin
ServerConfiguration.new(host: 'api.example.com', port: 99999)
rescue ArgumentError => e
puts e.message # => Port must be between 1 and 65535
end
Cache implementations show lazy initialization patterns. The constructor prepares the cache structure, but actual cache entries populate on first access rather than during construction.
class LazyCache
def initialize(capacity: 100, ttl: 3600)
@capacity = capacity
@ttl = ttl
@cache = {}
@access_times = {}
@mutex = Mutex.new
end
def fetch(key)
@mutex.synchronize do
if valid_entry?(key)
@access_times[key] = Time.now
return @cache[key]
end
value = yield
store(key, value)
value
end
end
def clear
@mutex.synchronize do
@cache.clear
@access_times.clear
end
end
def size
@cache.size
end
private
def valid_entry?(key)
return false unless @cache.key?(key)
return false if expired?(key)
true
end
def expired?(key)
return true unless @access_times.key?(key)
Time.now - @access_times[key] > @ttl
end
def store(key, value)
evict_lru if @cache.size >= @capacity
@cache[key] = value
@access_times[key] = Time.now
end
def evict_lru
oldest_key = @access_times.min_by { |k, v| v }[0]
@cache.delete(oldest_key)
@access_times.delete(oldest_key)
end
end
cache = LazyCache.new(capacity: 3, ttl: 60)
# Lazy initialization - value computed only when needed
result = cache.fetch('expensive_computation') do
sleep 1 # Simulate expensive operation
42
end
Common Patterns
The Factory Method pattern separates object creation from initialization logic. Class methods serve as named constructors in Ruby, providing semantic clarity for different creation scenarios.
class EmailMessage
attr_reader :to, :subject, :body, :from
private_class_method :new
def initialize(from:, to:, subject:, body:)
@from = from
@to = to
@subject = subject
@body = body
@sent_at = nil
end
def self.draft(from:, to:, subject:)
new(from: from, to: to, subject: subject, body: '')
end
def self.reply(original:, from:, body:)
new(
from: from,
to: original.from,
subject: "Re: #{original.subject}",
body: body
)
end
def self.forward(original:, from:, to:, note: '')
new(
from: from,
to: to,
subject: "Fwd: #{original.subject}",
body: "#{note}\n\n#{original.body}"
)
end
def send!
@sent_at = Time.now
# Send logic here
end
end
# Semantic construction methods
draft = EmailMessage.draft(from: 'alice@example.com', to: 'bob@example.com', subject: 'Meeting')
reply = EmailMessage.reply(original: draft, from: 'bob@example.com', body: 'Confirmed')
Builder pattern addresses complex initialization with many optional parameters. Builders accumulate configuration through method chaining before constructing the final object.
class QueryBuilder
def initialize
@select_columns = ['*']
@from_table = nil
@where_clauses = []
@order_by = nil
@limit_value = nil
end
def select(*columns)
@select_columns = columns
self
end
def from(table)
@from_table = table
self
end
def where(condition)
@where_clauses << condition
self
end
def order(column, direction = 'ASC')
@order_by = "#{column} #{direction}"
self
end
def limit(count)
@limit_value = count
self
end
def build
raise 'FROM clause required' unless @from_table
sql = "SELECT #{@select_columns.join(', ')} FROM #{@from_table}"
sql += " WHERE #{@where_clauses.join(' AND ')}" if @where_clauses.any?
sql += " ORDER BY #{@order_by}" if @order_by
sql += " LIMIT #{@limit_value}" if @limit_value
Query.new(sql)
end
end
class Query
attr_reader :sql
def initialize(sql)
@sql = sql
end
def execute
# Execute query
puts "Executing: #{@sql}"
end
end
# Fluent interface for construction
query = QueryBuilder.new
.select('name', 'email', 'created_at')
.from('users')
.where('active = true')
.where('created_at > 2024-01-01')
.order('created_at', 'DESC')
.limit(10)
.build
query.execute
Prototype pattern uses initialize_copy to create variations of existing objects. This pattern proves efficient when object creation is expensive.
class ReportTemplate
attr_accessor :title, :sections, :style, :footer
def initialize
@title = 'Default Report'
@sections = []
@style = default_style
@footer = default_footer
end
def initialize_copy(original)
super
@sections = original.sections.map(&:dup)
@style = original.style.dup
@footer = original.footer.dup
end
def add_section(name, content)
@sections << { name: name, content: content }
end
def generate
output = "# #{@title}\n\n"
@sections.each do |section|
output += "## #{section[:name]}\n"
output += "#{section[:content]}\n\n"
end
output += @footer
output
end
private
def default_style
{ font: 'Arial', size: 12, color: 'black' }
end
def default_footer
"Generated on #{Time.now}"
end
end
# Create prototype
template = ReportTemplate.new
template.title = 'Monthly Report'
template.add_section('Summary', 'Overview of monthly activities')
template.add_section('Metrics', 'Key performance indicators')
# Clone and customize
january_report = template.dup
january_report.title = 'January 2024 Report'
february_report = template.dup
february_report.title = 'February 2024 Report'
Singleton pattern restricts instantiation to a single object. Ruby provides a Singleton module that handles construction and instance management.
require 'singleton'
class ApplicationConfig
include Singleton
attr_reader :settings
def initialize
@settings = load_settings
@loaded_at = Time.now
end
def get(key)
@settings[key]
end
def set(key, value)
@settings[key] = value
end
def reload
@settings = load_settings
@loaded_at = Time.now
end
private
def load_settings
# Load from configuration file
{
environment: 'development',
database_url: 'postgresql://localhost/myapp',
cache_enabled: true
}
end
end
# Only one instance exists
config1 = ApplicationConfig.instance
config2 = ApplicationConfig.instance
puts config1.object_id == config2.object_id # => true
# ApplicationConfig.new raises NoMethodError
Error Handling & Edge Cases
Constructor failures require careful handling because partially initialized objects create inconsistent states. Ruby raises exceptions to prevent invalid object creation, leaving no object reference when initialization fails.
class DatabaseConnection
def initialize(host, port, credentials)
@host = validate_host(host)
@port = validate_port(port)
@credentials = credentials
@connection = nil
begin
@connection = establish_connection
rescue ConnectionError => e
raise "Failed to connect to #{@host}:#{@port} - #{e.message}"
end
end
def close
@connection&.close
end
private
def validate_host(host)
raise ArgumentError, 'Host required' if host.nil? || host.empty?
host
end
def validate_port(port)
port = port.to_i
raise ArgumentError, 'Invalid port' unless (1..65535).include?(port)
port
end
def establish_connection
# Connection logic that may fail
raise ConnectionError, 'Connection refused' if @host == 'invalid'
OpenStruct.new(close: -> {})
end
end
# Failed initialization leaves no object
begin
conn = DatabaseConnection.new('invalid', 5432, 'secret')
rescue => e
puts "Connection failed: #{e.message}"
puts "conn variable: #{defined?(conn) ? conn : 'undefined'}"
end
Circular dependencies during initialization cause infinite recursion. Objects should not create instances of their own class in constructors without termination conditions.
class Node
attr_accessor :value, :next_node
def initialize(value, create_next: false)
@value = value
@next_node = create_next ? Node.new(value + 1, create_next: false) : nil
end
end
# Safe - explicit termination
head = Node.new(1, create_next: true)
puts head.next_node.value # => 2
# Dangerous - would cause infinite recursion
class InfiniteNode
def initialize(value)
@value = value
@child = InfiniteNode.new(value + 1) # Never terminates!
end
end
# Don't run: InfiniteNode.new(1) causes stack overflow
Exception handling in constructors should clean up partially allocated resources. The ensure clause guarantees cleanup even when exceptions occur mid-initialization.
class ResourceManager
def initialize(resource_count)
@resources = []
@resource_count = resource_count
begin
resource_count.times do |i|
resource = allocate_resource(i)
@resources << resource
end
rescue => e
cleanup_partial_initialization
raise "Resource allocation failed at index #{@resources.size}: #{e.message}"
end
end
def close
@resources.each { |r| release_resource(r) }
@resources.clear
end
private
def allocate_resource(index)
raise 'Allocation failed' if index == 3 # Simulate failure
{ id: index, data: "Resource #{index}" }
end
def release_resource(resource)
# Cleanup logic
end
def cleanup_partial_initialization
@resources.each { |r| release_resource(r) }
@resources.clear
end
end
# Partial initialization cleaned up automatically
begin
manager = ResourceManager.new(5)
rescue => e
puts e.message # => Resource allocation failed at index 3: Allocation failed
end
Thread safety during initialization requires synchronization when constructors access shared state. The double-checked locking pattern initializes singleton instances safely across threads.
class ThreadSafeSingleton
@instance = nil
@mutex = Mutex.new
private_class_method :new
def self.instance
return @instance if @instance
@mutex.synchronize do
@instance ||= new
end
end
def initialize
@initialized_at = Time.now
@thread_id = Thread.current.object_id
sleep 0.1 # Simulate expensive initialization
end
attr_reader :initialized_at, :thread_id
end
# Multiple threads get same instance
threads = 10.times.map do
Thread.new do
ThreadSafeSingleton.instance
end
end
instances = threads.map(&:value)
puts instances.map(&:object_id).uniq.size # => 1 (all same instance)
Deep copying requires recursive duplication of nested structures. Shallow copies share references, causing modifications to propagate unexpectedly.
class Team
attr_reader :name, :members
def initialize(name)
@name = name
@members = []
end
def add_member(member)
@members << member
end
# Shallow copy - shares members array
def shallow_copy
self.dup
end
# Deep copy - duplicates members array
def initialize_copy(original)
super
@members = original.members.map(&:dup)
end
end
original = Team.new('Engineering')
original.add_member('Alice')
shallow = original.shallow_copy
shallow.add_member('Bob')
deep = original.dup
deep.add_member('Charlie')
puts "Original: #{original.members.inspect}" # => ["Alice", "Bob"]
puts "Shallow: #{shallow.members.inspect}" # => ["Alice", "Bob"]
puts "Deep: #{deep.members.inspect}" # => ["Alice", "Charlie"]
Common Pitfalls
Forgetting to call super in subclass constructors causes base class initialization to skip. Instance variables in the base class remain uninitialized, leading to nil reference errors.
class Vehicle
def initialize(wheels)
@wheels = wheels
@mileage = 0
end
def drive(distance)
@mileage += distance
end
end
class Bicycle < Vehicle
def initialize(gear_count)
# Missing super(2) call!
@gear_count = gear_count
end
end
bike = Bicycle.new(21)
begin
bike.drive(10)
rescue => e
puts "Error: #{e.class}" # NoMethodError: undefined method '+' for nil:NilClass
end
Assigning to instance variables before validation allows invalid states to exist temporarily. Validation should precede assignment to maintain object invariants.
# Wrong - invalid state created before validation
class BadAccount
def initialize(balance)
@balance = balance
validate_balance # Too late - invalid state already exists
end
private
def validate_balance
raise 'Balance cannot be negative' if @balance < 0
end
end
# Correct - validation before assignment
class GoodAccount
def initialize(balance)
validate_balance(balance)
@balance = balance
end
private
def validate_balance(value)
raise 'Balance cannot be negative' if value < 0
end
end
Using finalizers for critical cleanup causes resource leaks when garbage collection delays. Explicit cleanup methods provide deterministic resource management.
# Wrong - unreliable finalizer
class BadFileHandler
def initialize(filename)
@file = File.open(filename, 'w')
ObjectSpace.define_finalizer(self, proc { @file.close })
end
def write(data)
@file.write(data)
end
end
# File may remain open indefinitely
handler = BadFileHandler.new('output.txt')
handler.write('data')
handler = nil # Finalizer runs eventually, maybe
# Correct - explicit cleanup
class GoodFileHandler
def initialize(filename)
@file = File.open(filename, 'w')
end
def write(data)
@file.write(data)
end
def close
@file.close if @file && !@file.closed?
end
def self.open(filename)
handler = new(filename)
return handler unless block_given?
begin
yield handler
ensure
handler.close
end
end
end
# Guaranteed cleanup
GoodFileHandler.open('output.txt') do |handler|
handler.write('data')
end
Performing expensive operations in constructors increases instantiation time and complicates testing. Lazy initialization defers work until needed.
# Wrong - expensive initialization
class BadDataLoader
def initialize(file_path)
@file_path = file_path
@data = load_and_parse_data # Slow operation in constructor
end
private
def load_and_parse_data
sleep 2 # Simulate expensive operation
File.readlines(@file_path).map { |line| JSON.parse(line) }
end
end
# Every instantiation pays the cost
loader = BadDataLoader.new('data.json') # Waits 2 seconds
# Correct - lazy initialization
class GoodDataLoader
def initialize(file_path)
@file_path = file_path
@data = nil
end
def data
@data ||= load_and_parse_data
end
private
def load_and_parse_data
sleep 2 # Expensive operation
File.readlines(@file_path).map { |line| JSON.parse(line) }
end
end
# Instantiation fast, data loaded on first access
loader = GoodDataLoader.new('data.json') # Returns immediately
loader.data # Loads data when needed
Modifying constructor parameters affects the caller's data when parameters are mutable objects. Defensive copying prevents unintended coupling.
# Wrong - modifies caller's array
class BadContainer
def initialize(items)
@items = items
@items << 'default_item' # Modifies original array!
end
attr_reader :items
end
original_array = ['item1', 'item2']
container = BadContainer.new(original_array)
puts original_array.inspect # => ["item1", "item2", "default_item"]
# Correct - defensive copy
class GoodContainer
def initialize(items)
@items = items.dup
@items << 'default_item'
end
attr_reader :items
end
original_array = ['item1', 'item2']
container = GoodContainer.new(original_array)
puts original_array.inspect # => ["item1", "item2"]
puts container.items.inspect # => ["item1", "item2", "default_item"]
Reference
Constructor Components
| Component | Purpose | Ruby Syntax |
|---|---|---|
| Memory allocation | Reserve space for object | new method |
| Initialization | Set initial state | initialize method |
| Parameter passing | Provide initialization data | Method arguments |
| Validation | Ensure valid state | Raise exceptions |
| Resource acquisition | Obtain external resources | File.open, network connections |
| Inheritance chain | Initialize parent classes | super keyword |
Common Constructor Patterns
| Pattern | Use Case | Implementation |
|---|---|---|
| Default constructor | Simple initialization with defaults | def initialize; @var = default; end |
| Parameterized constructor | Custom initialization data | def initialize(param); @var = param; end |
| Named constructor | Semantic creation methods | def self.from_config(config); new(config[:value]); end |
| Copy constructor | Duplicate existing objects | def initialize_copy(original); super; @var = original.var.dup; end |
| Builder pattern | Complex configuration | Separate builder class with method chaining |
| Factory method | Polymorphic construction | Class method returning appropriate subclass |
Initialization Order
| Phase | Action | Timing |
|---|---|---|
| Allocation | Memory reserved | Before initialize |
| Base class | Parent initialize called | Via super keyword |
| Instance variables | Variables assigned | During initialize |
| Validation | Invariants checked | During or after assignment |
| Registration | Object registered in collections | After complete initialization |
| Return | Instance returned to caller | After initialize completes |
Cleanup Mechanisms
| Mechanism | Timing | Use Case |
|---|---|---|
| Explicit method | Immediate on call | Critical resource cleanup |
| Block pattern | Automatic with ensure | File and connection handling |
| Finalizer | Non-deterministic GC | Non-critical cleanup only |
| at_exit hook | Program termination | Application-level cleanup |
| Signal handlers | Process signals | Graceful shutdown |
Instance Variable Behavior
| Scenario | Behavior | Example |
|---|---|---|
| First assignment | Variable created | @var = value creates @var |
| Before assignment | Returns nil | @unassigned returns nil |
| Conditional creation | May or may not exist | @cache = {} if enabled |
| Inheritance | Not automatically inherited | Subclass must initialize |
| Access control | Always private | No public instance variables |
| Naming | Must start with @ | @valid, not valid |
Constructor Parameter Patterns
| Pattern | Syntax | Advantage |
|---|---|---|
| Required positional | def initialize(a, b) | Clear order, concise |
| Optional positional | def initialize(a, b = default) | Backward compatible |
| Keyword arguments | def initialize(key: value) | Self-documenting, flexible order |
| Keyword with defaults | def initialize(key: default) | Optional configuration |
| Splat arguments | def initialize(*args) | Variable argument count |
| Double splat | def initialize(**options) | Arbitrary keyword arguments |
| Block parameter | def initialize(&block) | Callback or configuration |
Deep vs Shallow Copy
| Aspect | Shallow Copy | Deep Copy |
|---|---|---|
| Simple values | Copied | Copied |
| Object references | Shared with original | Duplicated recursively |
| Method | dup or clone | Custom initialize_copy |
| Performance | Fast | Slower, recursive |
| Independence | Partially independent | Fully independent |
| Default behavior | Ruby default | Requires implementation |
Validation Strategies
| Strategy | Timing | Error Handling |
|---|---|---|
| Pre-assignment | Before @var = value | Raise before state change |
| Post-assignment | After all assignments | Check complete state |
| Progressive | After each assignment | Fail fast |
| Deferred | On first use | Lazy validation |
| External | Separate validator object | Reusable validation |
| Type checking | Parameter conversion | Explicit type coercion |
Thread-Safety Considerations
| Concern | Risk | Solution |
|---|---|---|
| Shared state modification | Race conditions | Mutex synchronization |
| Singleton initialization | Multiple instances | Double-checked locking |
| Resource allocation | Duplicate allocation | Atomic operations |
| Counter initialization | Lost updates | Thread-local storage |
| Collection initialization | Concurrent modification | Synchronized collections |
| Lazy initialization | Race conditions | Mutex or eager loading |