CrackedRuby logo

CrackedRuby

Constructors (initialize)

Overview

Ruby constructors control object creation and initialization through the initialize method. When new is called on a class, Ruby allocates memory for the object, then automatically invokes initialize if defined. The initialize method cannot be called directly on instances and serves as the primary mechanism for setting initial object state.

The constructor system centers around method definition patterns, parameter handling, and inheritance behavior. Ruby passes arguments from new directly to initialize, enabling flexible object creation patterns. The method returns the newly created object regardless of what initialize itself returns.

class Book
  def initialize(title, author)
    @title = title
    @author = author
  end
end

book = Book.new("1984", "Orwell")
# => #<Book:0x00007fb8b1234567 @title="1984", @author="Orwell">

Ruby supports constructor overloading through optional parameters, keyword arguments, and variable argument lists. Default values, splat operators, and keyword argument patterns provide multiple approaches to flexible object initialization.

class Database
  def initialize(host = 'localhost', port: 5432, **options)
    @host = host
    @port = port
    @options = options
  end
end

db1 = Database.new
db2 = Database.new('remote.example.com', port: 3306, ssl: true)

Constructor inheritance follows Ruby's method inheritance rules. Subclasses inherit parent constructors unless they define their own initialize method. The super keyword enables calling parent constructors from child classes, maintaining initialization chains across inheritance hierarchies.

Basic Usage

Constructor definition follows standard Ruby method syntax with the reserved name initialize. The method accepts any combination of positional arguments, keyword arguments, and blocks. Instance variables typically get assigned within the constructor to establish object state.

class User
  def initialize(name, email, age = nil)
    @name = name
    @email = email
    @age = age
    @created_at = Time.now
  end
  
  attr_reader :name, :email, :age, :created_at
end

user = User.new("Alice", "alice@example.com", 30)
user.name # => "Alice"

Keyword arguments provide named parameter access, improving readability for constructors with many parameters. Ruby supports both required and optional keyword arguments, with default values specified using standard assignment syntax.

class ServerConfig
  def initialize(host:, port: 80, ssl: false, timeout: 30)
    @host = host
    @port = port
    @ssl = ssl
    @timeout = timeout
  end
  
  def url
    protocol = @ssl ? 'https' : 'http'
    "#{protocol}://#{@host}:#{@port}"
  end
end

config = ServerConfig.new(host: 'api.example.com', ssl: true, port: 443)
config.url # => "https://api.example.com:443"

Variable argument lists handle dynamic parameter counts through splat operators. The *args pattern collects positional arguments into an array, while **kwargs collects keyword arguments into a hash. These patterns enable flexible constructor interfaces.

class Logger
  def initialize(*tags, level: :info, **metadata)
    @tags = tags
    @level = level
    @metadata = metadata
  end
  
  def log(message)
    tag_str = @tags.empty? ? "" : "[#{@tags.join(', ')}] "
    puts "#{@level.upcase}: #{tag_str}#{message}"
    puts "Metadata: #{@metadata}" unless @metadata.empty?
  end
end

logger = Logger.new('auth', 'api', level: :debug, service: 'web', version: '2.1')
logger.log("User authenticated")
# => DEBUG: [auth, api] User authenticated
# => Metadata: {:service=>"web", :version=>"2.1"}

Block parameters in constructors enable initialization-time configuration patterns. The constructor can yield to provided blocks, allowing callers to configure objects during creation through block execution.

class ConnectionPool
  def initialize(size = 5, &block)
    @size = size
    @connections = []
    @factory = block || -> { StandardConnection.new }
  end
  
  def acquire
    @connections.empty? ? @factory.call : @connections.pop
  end
end

pool = ConnectionPool.new(10) do
  CustomConnection.new(timeout: 5000, retries: 3)
end

Advanced Usage

Constructor patterns become complex when handling inheritance hierarchies, factory methods, and dynamic object creation. The super keyword controls parent constructor calls, with different behaviors based on argument passing patterns.

class Animal
  def initialize(name, species)
    @name = name
    @species = species
    @energy = 100
  end
end

class Dog < Animal
  def initialize(name, breed, owner = nil)
    super(name, 'Canine')  # Explicit arguments
    @breed = breed
    @owner = owner
    @tricks = []
  end
  
  def learn_trick(trick)
    @tricks << trick
    @energy -= 10
  end
end

class Cat < Animal
  def initialize(name, indoor: true, **traits)
    super(name, 'Feline')
    @indoor = indoor
    @traits = traits
    @territory = indoor ? :house : :neighborhood
  end
end

dog = Dog.new("Rex", "German Shepherd", "Alice")
cat = Cat.new("Whiskers", indoor: false, color: "tabby", temperament: "aloof")

Metaprogramming in constructors enables dynamic attribute assignment and configuration-driven object creation. The instance_variable_set method assigns instance variables from hash keys, while define_method creates accessors dynamically.

class ConfigurableModel
  def initialize(attributes = {}, **options)
    @strict_mode = options.fetch(:strict, false)
    
    attributes.each do |key, value|
      instance_var = "@#{key}".to_sym
      if @strict_mode && !self.class.valid_attributes.include?(key.to_sym)
        raise ArgumentError, "Invalid attribute: #{key}"
      end
      instance_variable_set(instance_var, value)
    end
    
    create_accessors(attributes.keys) if options[:create_accessors]
    validate_required_attributes if options[:validate]
  end
  
  def self.valid_attributes(*attrs)
    @valid_attributes = attrs if attrs.any?
    @valid_attributes ||= []
  end
  
  private
  
  def create_accessors(keys)
    keys.each do |key|
      self.class.define_method(key) { instance_variable_get("@#{key}") }
      self.class.define_method("#{key}=") { |val| instance_variable_set("@#{key}", val) }
    end
  end
  
  def validate_required_attributes
    required = self.class.instance_variable_get(:@required_attributes) || []
    required.each do |attr|
      unless instance_variable_defined?("@#{attr}")
        raise ArgumentError, "Required attribute missing: #{attr}"
      end
    end
  end
end

class User < ConfigurableModel
  valid_attributes :name, :email, :role, :active
  @required_attributes = [:name, :email]
end

user = User.new(
  { name: "John", email: "john@example.com", role: "admin" },
  strict: true,
  create_accessors: true,
  validate: true
)

Factory patterns within constructors handle complex object creation logic, conditional instantiation, and object composition. Class methods can serve as alternative constructors, providing semantic interfaces for different creation contexts.

class DatabaseConnection
  attr_reader :adapter, :host, :database, :pool_size
  
  def initialize(adapter:, host:, database:, pool_size: 5, **connection_opts)
    @adapter = adapter
    @host = host
    @database = database
    @pool_size = pool_size
    @connection_opts = connection_opts
    @connected = false
    
    establish_connection
  end
  
  def self.from_url(url, **options)
    uri = URI.parse(url)
    adapter = uri.scheme
    host = uri.host
    database = uri.path[1..-1]
    
    connection_opts = {}
    if uri.userinfo
      connection_opts[:username], connection_opts[:password] = uri.userinfo.split(':')
    end
    connection_opts[:port] = uri.port if uri.port
    
    new(
      adapter: adapter,
      host: host,
      database: database,
      **connection_opts.merge(options)
    )
  end
  
  def self.from_config(config_hash)
    required_keys = [:adapter, :host, :database]
    missing_keys = required_keys - config_hash.keys
    
    raise ArgumentError, "Missing configuration keys: #{missing_keys}" if missing_keys.any?
    
    new(**config_hash)
  end
  
  private
  
  def establish_connection
    case @adapter
    when 'postgresql', 'postgres'
      connect_postgresql
    when 'mysql', 'mysql2'
      connect_mysql  
    when 'sqlite', 'sqlite3'
      connect_sqlite
    else
      raise ArgumentError, "Unsupported adapter: #{@adapter}"
    end
    @connected = true
  end
end

# Usage patterns
db1 = DatabaseConnection.from_url("postgresql://user:pass@localhost/myapp")
db2 = DatabaseConnection.from_config(
  adapter: 'mysql2',
  host: 'db.example.com', 
  database: 'production',
  pool_size: 20
)

Constructor chaining patterns handle complex initialization sequences where objects depend on other objects or require multi-step setup processes. Builder patterns integrate with constructors to provide fluent interfaces for object configuration.

class HTTPClient
  def initialize(base_url, **options)
    @base_url = base_url
    @timeout = options[:timeout] || 30
    @retry_count = options[:retries] || 3
    @headers = options[:headers] || {}
    @middleware = []
    @interceptors = []
    
    configure_defaults
    yield(self) if block_given?
    finalize_setup
  end
  
  def add_middleware(middleware)
    @middleware << middleware
    self
  end
  
  def add_interceptor(&block)
    @interceptors << block
    self
  end
  
  def with_auth(token)
    @headers['Authorization'] = "Bearer #{token}"
    self
  end
  
  private
  
  def configure_defaults
    @headers['User-Agent'] ||= 'Ruby HTTP Client 1.0'
    @headers['Accept'] ||= 'application/json'
  end
  
  def finalize_setup
    validate_configuration
    setup_connection_pool
    register_cleanup_handlers
  end
  
  def validate_configuration
    raise ArgumentError, "Invalid timeout: #{@timeout}" if @timeout <= 0
    raise ArgumentError, "Invalid retry count: #{@retry_count}" if @retry_count < 0
  end
end

client = HTTPClient.new('https://api.example.com', timeout: 60, retries: 5) do |c|
  c.add_middleware(LoggingMiddleware.new)
   .add_middleware(RetryMiddleware.new)
   .with_auth('abc123')
   .add_interceptor { |req| req.headers['X-Request-ID'] = SecureRandom.uuid }
end

Error Handling & Debugging

Constructor error handling centers around argument validation, dependency checking, and resource allocation failures. Ruby raises ArgumentError for parameter mismatches, but custom validation requires explicit error handling within initialize methods.

class BankAccount
  attr_reader :account_number, :balance, :account_type
  
  def initialize(account_number, initial_balance = 0, account_type: :checking)
    validate_account_number(account_number)
    validate_initial_balance(initial_balance)
    validate_account_type(account_type)
    
    @account_number = account_number
    @balance = initial_balance.to_f
    @account_type = account_type
    @transactions = []
    @created_at = Time.now
    
    register_account
  rescue StandardError => e
    cleanup_partial_initialization
    raise e
  end
  
  private
  
  def validate_account_number(number)
    raise ArgumentError, "Account number cannot be nil" if number.nil?
    raise ArgumentError, "Account number must be a string" unless number.is_a?(String)
    raise ArgumentError, "Invalid account number format" unless number.match?(/\A\d{10,12}\z/)
    raise ArgumentError, "Account number already exists" if self.class.exists?(number)
  end
  
  def validate_initial_balance(balance)
    raise ArgumentError, "Initial balance cannot be nil" if balance.nil?
    raise TypeError, "Initial balance must be numeric" unless balance.respond_to?(:to_f)
    raise ArgumentError, "Initial balance cannot be negative" if balance.to_f < 0
    raise ArgumentError, "Initial balance exceeds maximum" if balance.to_f > 1_000_000
  end
  
  def validate_account_type(type)
    valid_types = [:checking, :savings, :business, :investment]
    raise ArgumentError, "Invalid account type: #{type}" unless valid_types.include?(type)
  end
  
  def register_account
    AccountRegistry.register(self)
  rescue RegistrationError => e
    raise ArgumentError, "Failed to register account: #{e.message}"
  end
  
  def cleanup_partial_initialization
    AccountRegistry.unregister(@account_number) if @account_number
    @transactions&.clear
  end
  
  def self.exists?(account_number)
    AccountRegistry.exists?(account_number)
  end
end

# Error scenarios
begin
  BankAccount.new("123", -100)
rescue ArgumentError => e
  puts "Validation failed: #{e.message}"
end

begin
  BankAccount.new("12345", 1000, account_type: :invalid)
rescue ArgumentError => e
  puts "Invalid configuration: #{e.message}"
end

Resource management in constructors requires careful handling of external resources, network connections, and file handles. Ruby's automatic garbage collection doesn't guarantee resource cleanup timing, making explicit resource management critical for constructors that allocate external resources.

class FileProcessor
  def initialize(input_path, output_path, **options)
    @input_path = validate_path(input_path, :read)
    @output_path = validate_path(output_path, :write) 
    @chunk_size = options[:chunk_size] || 8192
    @temp_files = []
    @resources = []
    
    begin
      setup_processing_environment
      allocate_buffers
      create_temp_directory
      open_file_handles
    rescue => error
      cleanup_resources
      raise ProcessingError, "Failed to initialize processor: #{error.message}"
    end
  end
  
  def finalize
    cleanup_resources
  end
  
  private
  
  def validate_path(path, mode)
    raise ArgumentError, "Path cannot be nil" if path.nil?
    raise ArgumentError, "Path must be a string" unless path.is_a?(String)
    
    case mode
    when :read
      raise ArgumentError, "Input file does not exist: #{path}" unless File.exist?(path)
      raise ArgumentError, "Input file is not readable: #{path}" unless File.readable?(path)
    when :write
      dir = File.dirname(path)
      raise ArgumentError, "Output directory does not exist: #{dir}" unless Dir.exist?(dir)
      raise ArgumentError, "Output directory is not writable: #{dir}" unless File.writable?(dir)
    end
    
    File.expand_path(path)
  end
  
  def setup_processing_environment
    @temp_dir = Dir.mktmpdir('file_processor')
    @resources << @temp_dir
  end
  
  def allocate_buffers  
    @input_buffer = ' ' * @chunk_size
    @output_buffer = ' ' * @chunk_size
  rescue NoMemoryError
    raise ProcessingError, "Insufficient memory for buffers"
  end
  
  def create_temp_directory
    @work_dir = File.join(@temp_dir, 'work')
    Dir.mkdir(@work_dir)
  rescue SystemCallError => e
    raise ProcessingError, "Cannot create work directory: #{e.message}"
  end
  
  def open_file_handles
    @input_file = File.open(@input_path, 'rb')
    @resources << @input_file
    
    @output_file = File.open(@output_path, 'wb')
    @resources << @output_file
  rescue SystemCallError => e
    raise ProcessingError, "Cannot open files: #{e.message}"
  end
  
  def cleanup_resources
    @resources.reverse_each do |resource|
      case resource
      when IO
        resource.close unless resource.closed?
      when String
        FileUtils.rm_rf(resource) if Dir.exist?(resource)
      end
    rescue => e
      warn "Warning: Failed to cleanup resource #{resource}: #{e.message}"
    end
    @resources.clear
  end
  
  class ProcessingError < StandardError; end
end

Debugging constructor issues requires systematic approaches to identify initialization failures, parameter problems, and state inconsistencies. Logging, introspection, and debugging hooks provide visibility into constructor execution.

class DebuggableService
  DEBUG_MODE = ENV['DEBUG_CONSTRUCTORS'] == 'true'
  
  def initialize(config, **options)
    @debug_info = { start_time: Time.now, steps: [] } if DEBUG_MODE
    
    debug_step("Starting initialization")
    debug_step("Config: #{config.inspect}")
    debug_step("Options: #{options.inspect}")
    
    begin
      @config = normalize_config(config)
      debug_step("Config normalized: #{@config.inspect}")
      
      @dependencies = resolve_dependencies(options[:dependencies] || [])
      debug_step("Dependencies resolved: #{@dependencies.keys}")
      
      @state = initialize_state(options[:initial_state])
      debug_step("State initialized: #{@state.inspect}")
      
      validate_configuration
      debug_step("Configuration validated")
      
    rescue => error
      debug_step("Error occurred: #{error.class} - #{error.message}")
      debug_step("Backtrace: #{error.backtrace.first(5).join(', ')}")
      dump_debug_info if DEBUG_MODE
      raise error
    end
    
    debug_step("Initialization completed successfully")
    dump_debug_info if DEBUG_MODE
  end
  
  def debug_info
    @debug_info
  end
  
  private
  
  def debug_step(message)
    return unless DEBUG_MODE
    
    @debug_info[:steps] << {
      step: @debug_info[:steps].length + 1,
      timestamp: Time.now,
      message: message,
      memory_usage: get_memory_usage,
      instance_variables: instance_variables.map { |var| 
        [var, instance_variable_defined?(var)]
      }.to_h
    }
  end
  
  def dump_debug_info
    total_time = Time.now - @debug_info[:start_time]
    
    puts "\n=== Constructor Debug Information ==="
    puts "Total initialization time: #{total_time.round(4)}s"
    puts "Steps executed: #{@debug_info[:steps].length}"
    puts
    
    @debug_info[:steps].each do |step|
      elapsed = step[:timestamp] - @debug_info[:start_time]
      puts "Step #{step[:step]} (#{elapsed.round(4)}s): #{step[:message]}"
      puts "  Memory: #{step[:memory_usage]}KB" if step[:memory_usage]
      puts "  Instance vars: #{step[:instance_variables].keys.join(', ')}"
    end
    puts "=== End Debug Information ===\n"
  end
  
  def get_memory_usage
    return nil unless respond_to?(:memory_usage, true)
    
    GC.start
    ObjectSpace.memsize_of(self) / 1024
  rescue
    nil
  end
  
  def normalize_config(config)
    case config
    when String
      { url: config }
    when Hash
      config.dup
    else
      raise ArgumentError, "Config must be String or Hash, got #{config.class}"
    end
  end
  
  def resolve_dependencies(dep_list)
    dep_list.each_with_object({}) do |dep_name, resolved|
      resolved[dep_name] = DependencyContainer.resolve(dep_name)
    end
  rescue DependencyError => e
    raise ArgumentError, "Dependency resolution failed: #{e.message}"
  end
  
  def initialize_state(initial_state)
    case initial_state
    when nil
      {}
    when Hash
      initial_state.dup
    else
      raise ArgumentError, "Initial state must be Hash or nil"
    end
  end
  
  def validate_configuration
    required_keys = [:url, :timeout]
    missing_keys = required_keys - @config.keys
    raise ArgumentError, "Missing required config: #{missing_keys}" if missing_keys.any?
  end
end

# Debug usage
service = DebuggableService.new(
  { url: 'http://example.com', timeout: 30 },
  dependencies: [:logger, :cache],
  initial_state: { retries: 3 }
)

Common Pitfalls

Constructor parameter handling creates subtle issues around argument passing, default value evaluation, and mutable default objects. Ruby evaluates default values at method definition time, not invocation time, causing shared mutable defaults across instances.

# WRONG - Shared mutable default
class ShoppingCart
  def initialize(items = [])
    @items = items
  end
  
  def add_item(item)
    @items << item
  end
end

cart1 = ShoppingCart.new
cart2 = ShoppingCart.new
cart1.add_item("apple")
cart2.instance_variable_get(:@items) # => ["apple"] - UNEXPECTED!

# CORRECT - New default per instance
class ShoppingCart
  def initialize(items = nil)
    @items = items || []
  end
  
  def add_item(item)
    @items << item
  end
end

# BETTER - Explicit nil default
class ShoppingCart
  def initialize(items: nil)
    @items = items&.dup || []
  end
end

Inheritance and super calls produce unexpected behavior when parameter lists change between parent and child constructors. Implicit super calls pass all arguments, while explicit super calls control argument passing precisely.

class Vehicle
  def initialize(make, model, year)
    @make = make
    @model = model  
    @year = year
    @fuel_level = 100
  end
end

# WRONG - Implicit super passes all arguments
class Car < Vehicle
  def initialize(make, model, year, doors)
    @doors = doors
    super  # Passes make, model, year, doors to parent - ERROR!
  end
end

# CORRECT - Explicit argument control
class Car < Vehicle
  def initialize(make, model, year, doors)
    super(make, model, year)  # Only passes required arguments
    @doors = doors
  end
end

# COMPLEX - Keyword argument handling
class ElectricVehicle < Vehicle
  def initialize(make, model, year, battery_capacity:, **options)
    super(make, model, year)  # Positional args only
    @battery_capacity = battery_capacity
    @charging_speed = options[:charging_speed] || 'standard'
    @range = options[:range] || calculate_range
  end
  
  private
  
  def calculate_range
    @battery_capacity * 3.5  # Simple calculation
  end
end

Instance variable initialization timing creates issues when accessors or other methods get called during constructor execution. Uninitialized instance variables return nil, potentially causing unexpected behavior in dependent code.

class Temperature
  attr_reader :celsius, :fahrenheit
  
  def initialize(temp, unit = :celsius)
    case unit
    when :celsius
      @celsius = temp
      @fahrenheit = celsius_to_fahrenheit  # Uses accessor method
    when :fahrenheit  
      @fahrenheit = temp
      @celsius = fahrenheit_to_celsius    # Uses accessor method
    else
      raise ArgumentError, "Invalid unit: #{unit}"
    end
    
    validate_temperature  # Called after both values set
  end
  
  private
  
  def celsius_to_fahrenheit
    (@celsius * 9.0 / 5.0) + 32
  end
  
  def fahrenheit_to_celsius
    (@fahrenheit - 32) * 5.0 / 9.0
  end
  
  def validate_temperature
    if @celsius < -273.15
      raise ArgumentError, "Temperature below absolute zero: #{@celsius}°C"
    end
  end
end

# PROBLEM - Method ordering matters
class ProblematicTemperature
  def initialize(temp, unit = :celsius)
    validate_temperature  # Called before instance variables set - WRONG!
    
    case unit
    when :celsius
      @celsius = temp
      @fahrenheit = celsius_to_fahrenheit  
    when :fahrenheit
      @fahrenheit = temp
      @celsius = fahrenheit_to_celsius
    end
  end
  
  def validate_temperature
    # @celsius might be nil here!
    raise ArgumentError, "Invalid temperature" if @celsius.nil? || @celsius < -273.15
  end
end

Block handling in constructors creates scoping and variable access issues. Blocks execute in the caller's context, not the object's context, limiting access to instance variables and methods during initialization.

class ConfigLoader
  def initialize(config_path, &block)
    @config_path = config_path
    @config = {}
    @processors = []
    
    # WRONG - Block executes in caller context
    block.call(self) if block_given?
    
    load_config_file
    process_config
  end
  
  def add_processor(name, &processor_block)
    @processors << { name: name, block: processor_block }
  end
  
  def set_default(key, value)
    @config[key] ||= value  
  end
  
  private
  
  def load_config_file
    # Load implementation
  end
  
  def process_config
    @processors.each do |processor|
      @config = processor[:block].call(@config)
    end
  end
end

# Usage reveals the problem
loader = ConfigLoader.new('app.yml') do |config|
  # This block runs in caller's context
  config.add_processor(:normalize) { |data| normalize_keys(data) }  # ERROR if normalize_keys not defined here
  config.set_default(:timeout, 30)
end

# BETTER - Provide explicit API
class ConfigLoader
  def initialize(config_path)
    @config_path = config_path
    @config = {}
    @processors = []
    
    yield(self) if block_given?  # Explicit yielding
    
    load_config_file
    process_config
  end
  
  def configure(&block)
    instance_eval(&block)  # Execute in object context
  end
end

# Usage with better scoping
loader = ConfigLoader.new('app.yml') do |config_loader|
  config_loader.configure do
    add_processor(:normalize) { |data| data.transform_keys(&:to_sym) }
    set_default(:timeout, 30)
  end
end

Constructor exceptions and cleanup present challenges around partial object initialization and resource management. Objects in partially constructed states can leak resources or remain in inconsistent states if error handling doesn't address cleanup properly.

class NetworkService
  def initialize(host, port, **options)
    @host = host
    @port = port
    @options = options
    @resources = []
    @initialized = false
    
    begin
      establish_connection
      setup_authentication
      configure_protocols
      register_handlers
      @initialized = true
    rescue => error
      cleanup_resources
      raise NetworkServiceError, "Initialization failed: #{error.message}"
    end
  end
  
  def finalize
    cleanup_resources if @initialized
  end
  
  private
  
  def establish_connection
    @socket = TCPSocket.new(@host, @port)
    @resources << @socket
  rescue SocketError => e
    raise ConnectionError, "Cannot connect to #{@host}:#{@port} - #{e.message}"
  end
  
  def setup_authentication
    if @options[:auth_token]
      authenticate_with_token(@options[:auth_token])
    elsif @options[:username] && @options[:password]
      authenticate_with_credentials(@options[:username], @options[:password])
    end
  rescue AuthenticationError => e
    raise InitializationError, "Authentication failed: #{e.message}"
  end
  
  def authenticate_with_token(token)
    response = send_command("AUTH TOKEN #{token}")
    raise AuthenticationError, "Invalid token" unless response.success?
  end
  
  def cleanup_resources
    @resources.reverse_each do |resource|
      begin
        case resource
        when TCPSocket
          resource.close unless resource.closed?
        when IO
          resource.close unless resource.closed?  
        when Tempfile
          resource.unlink
        end
      rescue => e
        warn "Failed to cleanup resource: #{e.message}"
      end
    end
    @resources.clear
  end
  
  class NetworkServiceError < StandardError; end
  class ConnectionError < NetworkServiceError; end  
  class AuthenticationError < NetworkServiceError; end
  class InitializationError < NetworkServiceError; end
end

# Proper error handling usage
begin
  service = NetworkService.new('api.example.com', 443, auth_token: 'invalid')
rescue NetworkService::NetworkServiceError => e
  puts "Service initialization failed: #{e.message}"
  # Object was never fully created, no cleanup needed
end

Reference

Constructor Method Patterns

Pattern Syntax Description
initialize def initialize(params) Standard constructor method
Optional Parameters def initialize(required, optional = default) Parameters with default values
Keyword Arguments def initialize(required:, optional: default) Named parameter access
Variable Arguments def initialize(*args, **kwargs) Dynamic argument collection
Block Parameter def initialize(&block) Block acceptance in constructor

Parameter Types and Handling

Type Example Behavior
Required Positional def initialize(name, email) Must be provided in order
Optional Positional def initialize(name, age = 0) Default used if not provided
Required Keyword def initialize(name:, email:) Must be provided by name
Optional Keyword def initialize(timeout: 30) Default used if not provided
Splat Arguments def initialize(*items) Collects positional args into array
Double Splat def initialize(**options) Collects keyword args into hash

Inheritance and Super Patterns

Super Usage Behavior Example
super Passes all current arguments def initialize(a, b); super; end
super() Passes no arguments def initialize(a, b); super(); end
super(args) Passes specified arguments def initialize(a, b); super(a); end
No super call Parent constructor not called def initialize(a, b); @a = a; end

Common Constructor Validations

# Parameter presence validation
def initialize(name, email)
  raise ArgumentError, "Name cannot be nil" if name.nil?
  raise ArgumentError, "Name cannot be empty" if name.to_s.strip.empty?
  
  @name = name
  @email = email
end

# Type validation
def initialize(count)
  raise TypeError, "Count must be numeric" unless count.respond_to?(:to_i)
  raise ArgumentError, "Count must be positive" if count.to_i <= 0
  
  @count = count.to_i
end

# Range validation  
def initialize(percentage)
  unless (0..100).cover?(percentage)
    raise ArgumentError, "Percentage must be between 0 and 100"
  end
  
  @percentage = percentage
end

# Format validation
def initialize(email)
  unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
    raise ArgumentError, "Invalid email format"
  end
  
  @email = email
end

Error Classes and Handling

Error Class Usage Example
ArgumentError Invalid arguments raise ArgumentError, "Value out of range"
TypeError Wrong argument types raise TypeError, "Expected String, got #{val.class}"
StandardError General errors Custom subclasses for specific failures
NoMethodError Missing methods Typically raised by Ruby automatically

Constructor Debugging Techniques

# Debug logging
def initialize(config)
  puts "Initializing with config: #{config.inspect}" if ENV['DEBUG']
  @config = config
  puts "Instance variables: #{instance_variables}" if ENV['DEBUG']
end

# Parameter inspection
def initialize(**params)
  required = [:host, :port]
  provided = params.keys
  missing = required - provided
  extra = provided - required - [:timeout, :retries]  # known optional
  
  puts "Missing required: #{missing}" if missing.any?
  puts "Unknown parameters: #{extra}" if extra.any?
  
  # Continue with initialization
end

# State validation
def initialize(data)
  @data = data
  validate_state
end

private

def validate_state
  errors = []
  errors << "Data is nil" if @data.nil?
  errors << "Data is empty" if @data.respond_to?(:empty?) && @data.empty?
  
  raise ArgumentError, "Invalid state: #{errors.join(', ')}" if errors.any?
end

Memory and Performance Considerations

Concern Impact Mitigation
Large default objects Shared mutable state Use nil defaults, create in constructor
Complex validations Slow initialization Cache validation results, lazy validation
Resource allocation Memory leaks Implement cleanup, use finalizers
Deep inheritance Method resolution overhead Limit inheritance depth, prefer composition
Block execution Context switching Minimize block usage in constructors

Factory Method Patterns

class DatabaseConnection
  # Standard constructor
  def initialize(adapter:, host:, database:, **options)
    # Implementation
  end
  
  # URL-based factory
  def self.from_url(url)
    # Parse URL and call new
  end
  
  # Configuration-based factory  
  def self.from_config(config_hash)
    # Validate and call new
  end
  
  # Environment-based factory
  def self.from_env(prefix = 'DB')
    # Read ENV variables and call new
  end
  
  # Connection string factory
  def self.parse(connection_string)
    # Parse string format and call new
  end
end