CrackedRuby CrackedRuby

Static vs Instance Members

Overview

Static and instance members represent two distinct scopes for data and behavior in object-oriented programming. Instance members belong to individual objects created from a class, with each object maintaining its own copy of instance variables and accessing instance methods through object references. Static members (called class members in Ruby) belong to the class itself rather than to any particular object, shared across all instances of that class.

The distinction determines how data gets stored, accessed, and shared across a program. Instance members model object-specific state and behavior—attributes that vary between different objects of the same type. Static members model class-level state and behavior—characteristics or operations that remain consistent regardless of which instance accesses them, or that need no instance at all.

This design emerges from the fundamental separation between classes as templates and objects as instantiated entities. A User class might define instance variables for each user's name and email, while maintaining a static counter tracking the total number of users created. The counter belongs to the class concept itself, not to any individual user object.

class User
  @@user_count = 0

  def initialize(name, email)
    @name = name
    @email = email
    @@user_count += 1
  end

  def profile
    "#{@name} <#{@email}>"
  end

  def self.total_users
    @@user_count
  end
end

user1 = User.new("Alice", "alice@example.com")
user2 = User.new("Bob", "bob@example.com")

puts user1.profile        # => Alice <alice@example.com>
puts User.total_users     # => 2

The choice between static and instance members affects memory usage, thread safety, testability, and architectural clarity. Static members exist throughout the program's lifetime, allocated when the class loads. Instance members exist only during an object's lifetime, allocated at instantiation and garbage collected when no references remain.

Key Principles

Scope and Ownership

Instance members belong to specific object instances. Each object created from a class receives its own copy of instance variables, maintaining separate state. Instance methods operate on this per-object state through implicit access to the object's instance variables. Static members belong to the class itself, existing independently of any instances. Only one copy of static data exists in memory, shared by all objects of that class.

Lifecycle Management

Static members initialize when the class loads into memory, typically at program startup or first reference. They persist until program termination or explicit class unloading. Instance members initialize during object construction, exist throughout the object's lifetime, and deallocate when the object becomes eligible for garbage collection. This lifecycle difference affects resource management—static members holding resources require explicit cleanup, while instance member cleanup follows object lifecycle.

Access Patterns

Instance methods access both instance and static members of their class. They operate with implicit access to the current object through self, reaching instance variables directly and invoking other instance methods without qualification. Static methods access only static members and cannot directly access instance members, lacking any association with a particular object. Attempting to access instance state from a static context produces an error—the static method has no self reference.

Memory Allocation

Each object allocates memory for its own instance variables. Creating 1,000 objects from a class produces 1,000 separate copies of instance data. Static variables allocate memory once per class, regardless of instance count. A class with five static variables consumes the same static memory whether zero or one million instances exist. This allocation pattern makes static members efficient for shared state but problematic for per-object state.

Method Resolution

Ruby resolves instance method calls through the object's class and ancestor chain. When invoking object.method_name, Ruby searches the object's singleton class, then its class, then included modules, then the superclass chain. Static method calls resolve directly through the class reference without instance involvement. The call ClassName.method_name locates the method in the class's eigenclass (singleton class), bypassing instance method lookup entirely.

Inheritance Behavior

Subclasses inherit both static and instance members from parent classes. Instance members follow standard inheritance—subclass instances contain all inherited instance variables and access inherited instance methods through method lookup. Static members require more care. Ruby class variables (@@variable) share across the entire inheritance hierarchy, creating unexpected coupling. Class instance variables (@variable at class level) provide better isolation, giving each class in the hierarchy its own copy.

Ruby Implementation

Ruby implements static members through several mechanisms, each with distinct characteristics and use cases. Class methods define static behavior, while class variables and class instance variables handle static state.

Class Method Definition

Ruby provides three syntaxes for defining class methods. The def self.method_name syntax attaches the method to the class's singleton class, making it callable on the class object. The class << self syntax opens the singleton class for multiple method definitions. The def ClassName.method_name syntax explicitly names the receiver, used less frequently due to coupling with the class name.

class Database
  # Syntax 1: def self.method_name
  def self.connect(host, port)
    "Connecting to #{host}:#{port}"
  end

  # Syntax 2: class << self
  class << self
    def connection_pool_size
      10
    end

    def max_connections
      100
    end
  end

  # Syntax 3: explicit class name
  def Database.query_timeout
    30
  end
end

Database.connect("localhost", 5432)
# => "Connecting to localhost:5432"

Database.connection_pool_size
# => 10

The class << self syntax proves most useful when defining multiple class methods, avoiding repetition of self and grouping related static behavior. The explicit class name syntax becomes problematic if the class name changes, requiring updates to all method definitions.

Class Variables

Class variables (@@variable) share state across the entire class hierarchy, including the class and all its subclasses. Any class or instance method can read and modify class variables. This sharing creates tight coupling—changes in a subclass affect the parent class and all sibling subclasses.

class Counter
  @@count = 0

  def initialize
    @@count += 1
  end

  def self.total
    @@count
  end

  def self.reset
    @@count = 0
  end
end

class SpecialCounter < Counter
end

Counter.new
Counter.new
puts Counter.total  # => 2

SpecialCounter.new
puts Counter.total  # => 3
puts SpecialCounter.total  # => 3

Counter.reset
puts SpecialCounter.total  # => 0

The shared nature of class variables makes them unsuitable for most static state. Modifying the count in Counter affects SpecialCounter and vice versa. This coupling violates encapsulation and produces confusing behavior when inheritance complicates the class hierarchy.

Class Instance Variables

Class instance variables (@variable defined at class level) belong to the specific class object, not shared with subclasses. Each class in an inheritance hierarchy maintains its own class instance variables, providing better isolation. Accessor methods defined in the class's singleton class expose class instance variables.

class Configuration
  @environment = "development"
  @debug_mode = false

  class << self
    attr_accessor :environment, :debug_mode

    def database_config
      {
        host: @environment == "production" ? "prod-db.example.com" : "localhost",
        debug: @debug_mode
      }
    end
  end
end

class TestConfiguration < Configuration
  @environment = "test"
  @debug_mode = true
end

Configuration.environment    # => "development"
TestConfiguration.environment # => "test"

Configuration.environment = "staging"
Configuration.environment    # => "staging"
TestConfiguration.environment # => "test"

Class instance variables provide isolated static state per class. Changes to Configuration.environment do not affect TestConfiguration.environment. Each class maintains independent state, following the principle of least coupling.

Instance Variables

Instance variables (@variable) belong to individual objects, allocated during object instantiation. Each object maintains separate copies, accessible only through instance methods of that object. Instance variables do not require declaration—assignment creates them. Uninitialized instance variables return nil rather than raising an error.

class Account
  def initialize(account_number, balance)
    @account_number = account_number
    @balance = balance
  end

  def deposit(amount)
    @balance += amount
  end

  def withdraw(amount)
    return false if @balance < amount
    @balance -= amount
    true
  end

  def balance
    @balance
  end

  def account_number
    @account_number
  end
end

account1 = Account.new("12345", 1000)
account2 = Account.new("67890", 500)

account1.deposit(200)
account1.balance  # => 1200
account2.balance  # => 500

Each Account object maintains separate @account_number and @balance instance variables. Operations on account1 never affect account2. Instance variables encapsulate per-object state, the foundation of object-oriented programming.

Singleton Methods

Ruby allows defining methods on individual objects, creating singleton methods that exist only for that specific instance. These methods live in the object's singleton class, consulted before the object's actual class during method lookup. Singleton methods effectively create per-object static behavior.

server = "web-server-01"

def server.start
  "Starting #{self}"
end

def server.stop
  "Stopping #{self}"
end

server.start  # => "Starting web-server-01"

another_server = "web-server-02"
# another_server.start  # => NoMethodError

# Class methods are singleton methods on the class object
class Logger
end

def Logger.log(message)
  puts "[LOG] #{message}"
end

Logger.log("Application started")
# => [LOG] Application started

Singleton methods demonstrate that class methods represent a specific case of singleton methods—methods defined on a class object rather than an arbitrary object. This unifies Ruby's object model: classes are objects, and class methods are singleton methods on class objects.

Design Considerations

State Ownership and Mutability

Choose instance members for state that varies per object. User names, account balances, connection handles, and object-specific configuration belong to instances. Choose static members for state shared across all instances or conceptually belonging to the class itself. Configuration settings, caches, connection pools, and factory methods belong to the class level.

Mutable static state introduces coupling and thread safety concerns. Multiple threads accessing and modifying shared static variables require synchronization. Immutable static state avoids these issues—configuration constants, lookup tables, and factory registries work well as static members when immutable.

class ApiClient
  # Static configuration (immutable after initialization)
  @base_url = "https://api.example.com"
  @timeout = 30

  class << self
    attr_reader :base_url, :timeout

    def configure(base_url: nil, timeout: nil)
      @base_url = base_url if base_url
      @timeout = timeout if timeout
    end
  end

  # Instance state (per-connection data)
  def initialize(auth_token)
    @auth_token = auth_token
    @request_count = 0
  end

  def get(endpoint)
    @request_count += 1
    # Use self.class.base_url and @auth_token
    "GET #{self.class.base_url}#{endpoint} with #{@auth_token}"
  end

  def request_count
    @request_count
  end
end

ApiClient.configure(timeout: 60)
client1 = ApiClient.new("token-abc")
client2 = ApiClient.new("token-xyz")

client1.get("/users")
client1.request_count  # => 1
client2.request_count  # => 0

This design separates shared configuration (static) from per-client state (instance). Multiple clients share timeout and base URL settings while maintaining independent authentication and request tracking.

Memory and Performance Trade-offs

Static members consume memory once regardless of instance count. Applications creating thousands or millions of objects benefit from moving shared data to static members. A Color class representing RGB values might define static members for common colors rather than allocating duplicate objects.

class Color
  @colors = {}

  def self.[](name)
    @colors[name]
  end

  def self.define(name, r, g, b)
    @colors[name] = new(r, g, b)
  end

  def initialize(r, g, b)
    @r, @g, @b = r, g, b
  end

  attr_reader :r, :g, :b
end

Color.define(:red, 255, 0, 0)
Color.define(:green, 0, 255, 0)
Color.define(:blue, 0, 0, 255)

red1 = Color[:red]
red2 = Color[:red]
red1.object_id == red2.object_id  # => true

The static color registry ensures only one instance exists per color definition, saving memory when the same colors appear repeatedly throughout an application. This flyweight pattern trades a static lookup table for reduced instance allocation.

Testing and Dependency Injection

Static members complicate testing due to global state persistence across test cases. Static variables modified during one test affect subsequent tests unless explicitly reset. Instance members encapsulate state within test-specific objects, providing natural isolation.

Dependency injection works more naturally with instance members. Injecting dependencies through constructor parameters creates testable objects with configurable behavior. Static methods cannot receive injected dependencies without global state or additional indirection.

# Hard to test - static dependency
class OrderProcessor
  def self.process(order)
    PaymentGateway.charge(order.total)
    EmailService.send_confirmation(order.email)
    order.mark_complete
  end
end

# Testable - instance with dependency injection
class OrderProcessor
  def initialize(payment_gateway, email_service)
    @payment_gateway = payment_gateway
    @email_service = email_service
  end

  def process(order)
    @payment_gateway.charge(order.total)
    @email_service.send_confirmation(order.email)
    order.mark_complete
  end
end

# In tests
processor = OrderProcessor.new(MockPaymentGateway.new, MockEmailService.new)
processor.process(test_order)

The instance-based design allows injecting test doubles, verifying behavior without external dependencies. The static design requires stubbing global classes or complex setup to avoid actual payment and email operations during testing.

Inheritance and Polymorphism

Instance methods participate fully in polymorphism—subclasses override parent instance methods, and callers invoke the most specific implementation through dynamic dispatch. Static methods do not participate in polymorphism the same way. Ruby resolves static method calls directly to the specified class without considering the inheritance hierarchy.

class Animal
  def self.classification
    "Kingdom: Animalia"
  end

  def speak
    "Some sound"
  end
end

class Dog < Animal
  def self.classification
    "Kingdom: Animalia, Class: Mammalia, Order: Carnivora"
  end

  def speak
    "Woof"
  end
end

# Instance polymorphism works
animal = Dog.new
animal.speak  # => "Woof"

animals = [Dog.new, Animal.new]
animals.each { |a| puts a.speak }
# => Woof
# => Some sound

# Static methods don't participate in polymorphism the same way
Dog.classification
# => "Kingdom: Animalia, Class: Mammalia, Order: Carnivora"

# Calling via reference to parent doesn't invoke child implementation
klass = Dog
klass.classification
# => "Kingdom: Animalia, Class: Mammalia, Order: Carnivora"

Design static methods for operations independent of subclass specialization. Factory methods, configuration, and utility functions work well as static methods. Operations requiring different behavior per subclass belong in instance methods.

Common Patterns

Factory Methods

Static factory methods construct objects with specific configurations or from various input formats. Factory methods encapsulate complex instantiation logic, provide named constructors for clarity, and control object creation.

class Date
  def initialize(year, month, day)
    @year, @month, @day = year, month, day
  end

  def self.today
    time = Time.now
    new(time.year, time.month, time.day)
  end

  def self.from_string(date_string)
    year, month, day = date_string.split('-').map(&:to_i)
    new(year, month, day)
  end

  def self.from_timestamp(timestamp)
    time = Time.at(timestamp)
    new(time.year, time.month, time.day)
  end

  def to_s
    "#{@year}-#{@month.to_s.rjust(2, '0')}-#{@day.to_s.rjust(2, '0')}"
  end
end

date1 = Date.today
date2 = Date.from_string("2025-03-15")
date3 = Date.from_timestamp(1710460800)

Factory methods provide clear intent—Date.today communicates purpose better than Date.new(2025, 10, 11). They handle parsing, validation, and default values, simplifying object creation for callers.

Singleton Pattern

The singleton pattern ensures only one instance of a class exists, providing global access through a static method. Ruby's standard library includes a Singleton module implementing this pattern.

require 'singleton'

class Configuration
  include Singleton

  attr_accessor :database_url, :api_key, :log_level

  def initialize
    @database_url = "postgresql://localhost/myapp"
    @api_key = nil
    @log_level = :info
  end

  def load_from_file(path)
    # Load configuration from file
    @database_url = "postgresql://production-db/myapp"
    @api_key = "prod-key-12345"
  end
end

# Access the single instance
config = Configuration.instance
config.log_level = :debug

# Same instance everywhere
other_config = Configuration.instance
other_config.log_level  # => :debug

# Cannot create new instances
# Configuration.new  # => NoMethodError

The Singleton module makes the constructor private and provides a static instance method returning the sole instance. This pattern centralizes configuration, connection pools, and caches requiring single-instance semantics.

Class-Level Caching

Static caching stores computed results at class level, sharing cached data across all instances. This pattern improves performance for expensive operations producing identical results regardless of instance.

class GeocodingService
  @cache = {}

  def self.geocode(address)
    return @cache[address] if @cache.key?(address)

    # Expensive API call
    result = perform_geocoding(address)
    @cache[address] = result
    result
  end

  def self.clear_cache
    @cache.clear
  end

  private

  def self.perform_geocoding(address)
    # Simulate API call
    { lat: 40.7128, lon: -74.0060, address: address }
  end
end

# First call performs geocoding
result1 = GeocodingService.geocode("New York, NY")

# Subsequent calls use cached result
result2 = GeocodingService.geocode("New York, NY")
result1.object_id == result2.object_id  # => true

Class-level caching reduces redundant computation when multiple instances require the same data. Database query results, API responses, and computed values benefit from this pattern. Cache invalidation requires careful design—the static cache persists throughout program execution unless explicitly cleared.

Object Counters and Tracking

Class variables or class instance variables track object creation, maintaining counts or registries of created instances. This pattern implements object pools, generates unique identifiers, and monitors resource allocation.

class Connection
  @connections = []
  @connection_id = 0

  def self.active_connections
    @connections.select(&:open?)
  end

  def self.total_created
    @connection_id
  end

  def initialize(host, port)
    @host = host
    @port = port
    @open = true
    @id = self.class.next_connection_id
    self.class.register(self)
  end

  def close
    @open = false
  end

  def open?
    @open
  end

  attr_reader :id, :host, :port

  private

  def self.next_connection_id
    @connection_id += 1
  end

  def self.register(connection)
    @connections << connection
  end
end

conn1 = Connection.new("localhost", 5432)
conn2 = Connection.new("localhost", 5433)
conn3 = Connection.new("localhost", 5434)

conn1.close

Connection.active_connections.length  # => 2
Connection.total_created              # => 3

The static registry tracks all created connections, enabling resource monitoring and management. Static counters generate unique identifiers without coordination between instances.

Utility Classes

Utility classes contain only static methods, providing namespace grouping for related functions. These classes never instantiate—all functionality exists at class level.

class StringUtils
  def self.truncate(string, length, omission: "...")
    return string if string.length <= length
    string[0...(length - omission.length)] + omission
  end

  def self.slugify(string)
    string.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')
  end

  def self.word_count(string)
    string.split.length
  end

  private_class_method :new
end

StringUtils.truncate("Long text here", 10)
# => "Long te..."

StringUtils.slugify("Hello World!")
# => "hello-world"

# Cannot instantiate
# StringUtils.new  # => NoMethodError

Making the constructor private prevents instantiation, signaling the class's static-only nature. Utility classes organize related operations without maintaining state, similar to modules but providing namespace scoping through the class name.

Practical Examples

Configuration Management System

A configuration system demonstrates the interaction between static and instance members. Static configuration holds application-wide settings, while instance configuration applies settings to specific components.

class AppConfig
  # Static default configuration
  @defaults = {
    timeout: 30,
    retry_attempts: 3,
    log_level: :info
  }

  class << self
    attr_reader :defaults

    def set_default(key, value)
      @defaults[key] = value
    end

    def get_default(key)
      @defaults[key]
    end
  end

  # Instance-specific overrides
  def initialize(overrides = {})
    @config = self.class.defaults.merge(overrides)
  end

  def get(key)
    @config[key]
  end

  def set(key, value)
    @config[key] = value
  end

  def timeout
    @config[:timeout]
  end

  def retry_attempts
    @config[:retry_attempts]
  end
end

# Set application-wide defaults
AppConfig.set_default(:timeout, 60)

# Create configurations with instance-specific overrides
default_config = AppConfig.new
special_config = AppConfig.new(timeout: 120, retry_attempts: 5)

default_config.timeout    # => 60
special_config.timeout    # => 120

# Changes to static defaults don't affect existing instances
AppConfig.set_default(:timeout, 90)
default_config.timeout    # => 60

# New instances use updated defaults
new_config = AppConfig.new
new_config.timeout        # => 90

Static defaults provide baseline configuration, while instances customize settings for specific use cases. This separation allows component-level configuration without duplicating default values across every instance.

Connection Pool Implementation

Connection pools manage shared resources through static members while tracking individual connections via instance members.

class DatabaseConnection
  @pool = []
  @max_connections = 10
  @created_count = 0

  class << self
    def checkout
      if @pool.empty? && @created_count < @max_connections
        connection = new
        @created_count += 1
        connection
      elsif @pool.empty?
        nil
      else
        @pool.pop
      end
    end

    def checkin(connection)
      return unless connection.valid?
      @pool.push(connection) if @pool.size < @max_connections
    end

    def pool_size
      @pool.size
    end

    def total_created
      @created_count
    end
  end

  def initialize
    @id = SecureRandom.uuid
    @created_at = Time.now
    @query_count = 0
    @valid = true
  end

  def execute(query)
    return nil unless @valid
    @query_count += 1
    "Executing: #{query}"
  end

  def close
    @valid = false
  end

  def valid?
    @valid
  end

  attr_reader :id, :query_count, :created_at
end

# Checkout connections from the pool
conn1 = DatabaseConnection.checkout
conn2 = DatabaseConnection.checkout

conn1.execute("SELECT * FROM users")
conn2.execute("SELECT * FROM posts")

conn1.query_count  # => 1
conn2.query_count  # => 1

# Return connections to the pool
DatabaseConnection.checkin(conn1)
DatabaseConnection.pool_size  # => 1

# Reuse pooled connections
conn3 = DatabaseConnection.checkout
conn3.id == conn1.id  # => true

The static pool manages shared connections, enforcing maximum connection limits and providing reuse. Instance members track per-connection state—query counts, validity, and creation timestamps. This design balances resource sharing with per-connection state management.

Event Counter and Statistics

An event tracking system uses static members for aggregate statistics and instance members for per-event details.

class Event
  @total_events = 0
  @events_by_type = Hash.new(0)

  class << self
    attr_reader :total_events

    def statistics
      {
        total: @total_events,
        by_type: @events_by_type.dup
      }
    end

    def reset_statistics
      @total_events = 0
      @events_by_type.clear
    end

    private

    def record_event(event_type)
      @total_events += 1
      @events_by_type[event_type] += 1
    end
  end

  def initialize(event_type, metadata = {})
    @event_type = event_type
    @metadata = metadata
    @timestamp = Time.now
    @id = SecureRandom.uuid
    self.class.send(:record_event, event_type)
  end

  def details
    {
      id: @id,
      type: @event_type,
      timestamp: @timestamp,
      metadata: @metadata
    }
  end

  attr_reader :event_type, :timestamp, :id
end

# Create various events
event1 = Event.new(:user_login, user_id: 123)
event2 = Event.new(:user_login, user_id: 456)
event3 = Event.new(:purchase, amount: 99.99)
event4 = Event.new(:user_logout, user_id: 123)

# Instance details
event1.details
# => {:id=>"...", :type=>:user_login, :timestamp=>..., :metadata=>{:user_id=>123}}

# Aggregate statistics
Event.statistics
# => {:total=>4, :by_type=>{:user_login=>2, :purchase=>1, :user_logout=>1}}

Static counters aggregate data across all events while instance variables preserve per-event information. This pattern separates individual event tracking from system-wide analytics.

Reference

Member Type Comparison

Aspect Instance Members Static Members
Ownership Belongs to individual objects Belongs to the class itself
Allocation Per object instance Once per class
Lifetime Object creation to garbage collection Class load to program termination
Access from instance methods Direct access via self Access via self.class or ClassName
Access from static methods Cannot access directly Direct access
Memory usage Multiplied by instance count Fixed regardless of instance count
Inheritance Each subclass instance has own copy Shared or isolated depending on mechanism

Ruby Syntax Reference

Purpose Syntax Example
Instance variable @variable @name, @balance
Instance method def method_name def calculate
Class variable @@variable @@count
Class instance variable @variable at class level @config
Class method style 1 def self.method_name def self.create
Class method style 2 class << self class << self; def factory; end; end
Class method style 3 def ClassName.method_name def User.count
Instance variable reader attr_reader :variable attr_reader :name
Instance variable writer attr_writer :variable attr_writer :age
Instance variable accessor attr_accessor :variable attr_accessor :email

Access Patterns

From Context Can Access Cannot Access
Instance method Instance variables, instance methods, static members via self.class Nothing (full access)
Static method Static members, class variables, class instance variables Instance variables, instance methods (no self reference)
Outside class Public instance methods via object, public static methods via class Private members, instance variables directly

Variable Scoping Rules

Variable Type Visibility Shared Across Typical Use
Instance variable Within instance Not shared Per-object state
Class variable Class and all subclasses Entire hierarchy Shared counters (use carefully)
Class instance variable Within specific class Not with subclasses Per-class configuration
Local variable Within method Not shared Temporary computation

Common Method Patterns

Pattern Instance Method Static Method
Initialize object def initialize(params) N/A
Factory creation N/A def self.create(params)
Getter def attribute; @attribute; end def self.config; @config; end
Setter def attribute=(value); @attribute = value; end def self.config=(value); @config = value; end
Query state def active?; @status == :active; end def self.enabled?; @enabled; end
Modify state def activate; @status = :active; end def self.enable; @enabled = true; end
Computation with state def calculate; @value * 2; end def self.default_multiplier; 2; end
Delegation def process; @handler.execute; end def self.processor; @default_processor; end

Design Decision Matrix

Requirement Choose Instance Members Choose Static Members
State varies per object Yes No
State shared across objects No Yes
Behavior varies per object Yes No
Behavior independent of objects No Yes
Need polymorphic behavior Yes Limited
Factory or builder pattern No Yes
Resource pooling No Yes
Per-object lifecycle Yes No
Application-wide configuration No Yes
Testing with dependency injection Easier Harder