CrackedRuby CrackedRuby

Constructor and Destructor Concepts

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