CrackedRuby logo

CrackedRuby

Class Methods as Singleton Methods

Class methods in Ruby are implemented as singleton methods attached to the class object itself, providing a mechanism for defining behavior that belongs to the class rather than its instances.

Metaprogramming Singleton Classes
5.5.3

Overview

Ruby treats classes as first-class objects, meaning every class is an instance of the Class class. When you define a class method, Ruby creates a singleton method on the class object. This implementation detail explains why class methods behave differently from instance methods and why they can be defined in multiple ways.

The singleton method mechanism works by creating an invisible singleton class (also called eigenclass or metaclass) for each object. When you define a class method, Ruby attaches it to the singleton class of the class object. This approach provides Ruby with a unified object model where all method calls follow the same lookup pattern.

class User
  def self.find(id)
    # Class method implementation
  end
end

# Ruby creates a singleton method on the User class object
User.singleton_methods
# => [:find]

Class objects inherit from the Class class, which provides basic functionality like new, allocate, and superclass. The singleton methods you define get added to the class object's singleton class, sitting in the method lookup chain before the Class class methods.

class Product
  class << self
    def create(attributes)
      new(attributes).tap(&:save)
    end
  end
end

Product.singleton_class.instance_methods(false)
# => [:create]

Ruby provides several syntax options for defining class methods, all of which create singleton methods on the class object. The def self.method_name syntax directly defines a singleton method, while the class << self syntax opens the singleton class for method definitions.

Basic Usage

The most common syntax for defining class methods uses def self.method_name within the class body. This approach creates a singleton method on the class object, making it callable directly on the class.

class Order
  def self.pending
    # Returns pending orders
    where(status: 'pending')
  end

  def self.completed_today
    where(status: 'completed', created_at: Date.current.all_day)
  end
end

Order.pending
Order.completed_today

The class << self syntax opens the singleton class of the class object, allowing you to define multiple class methods within the same block. This approach proves useful when defining many class methods or when you need to access the singleton class directly.

class Invoice
  class << self
    def overdue
      where('due_date < ?', Date.current)
    end

    def for_customer(customer)
      where(customer: customer)
    end

    private

    def complex_calculation(data)
      # Private class method
      data.sum * 0.15
    end
  end
end

Class methods can accept parameters, blocks, and keyword arguments like instance methods. They operate on the class object itself rather than on individual instances, making them suitable for factory methods, queries, and operations that don't require instance state.

class Document
  def self.create_from_template(template_name, **attributes)
    template = find_template(template_name)
    new(attributes.merge(template.default_attributes))
  end

  def self.batch_process(documents, &processor)
    documents.each { |doc| processor.call(doc) }
  end

  private_class_method def self.find_template(name)
    # Implementation details
  end
end

doc = Document.create_from_template('invoice', customer: 'ACME Corp')
Document.batch_process(documents) { |d| d.process! }

You can define class methods outside the class body by opening the singleton class of the class object. This technique allows you to extend existing classes with new class methods.

class String
  class << self
    def random(length = 10)
      Array.new(length) { [*'a'..'z', *'A'..'Z', *'0'..'9'].sample }.join
    end
  end
end

String.random(8)
# => "aB3xY9mP"

Advanced Usage

Class methods participate in inheritance through the singleton class hierarchy. When a class inherits from another class, its singleton class inherits from the parent class's singleton class, making parent class methods available to child classes.

class Animal
  def self.species_count
    descendants.count
  end

  def self.descendants
    ObjectSpace.each_object(Class).select { |cls| cls < self }
  end
end

class Mammal < Animal
  def self.warm_blooded?
    true
  end
end

class Dog < Mammal
end

# Dog inherits Animal's class methods through singleton class inheritance
Dog.species_count
Dog.warm_blooded?  # Available through Mammal

Metaprogramming techniques can define class methods dynamically using define_singleton_method or by evaluating code in the context of the singleton class. This approach enables creating DSL-like interfaces and reducing code duplication.

class Configuration
  SETTINGS = %w[database_url redis_url secret_key].freeze

  class << self
    SETTINGS.each do |setting|
      define_method(setting) do
        ENV[setting.upcase]
      end

      define_method("#{setting}=") do |value|
        ENV[setting.upcase] = value
      end
    end
  end
end

Configuration.database_url = "postgresql://localhost/app"
Configuration.database_url
# => "postgresql://localhost/app"

Class methods can be aliased and undefined using singleton_class to access the singleton class directly. This technique provides fine-grained control over the class method interface.

class ApiClient
  def self.get(endpoint)
    # GET request implementation
  end

  singleton_class.alias_method :fetch, :get
  singleton_class.private :get

  def self.configure(&block)
    instance_eval(&block)
  end
end

ApiClient.fetch('/users')    # Works via alias
# ApiClient.get('/users')    # Raises NoMethodError (private)

Class method delegation can be achieved by forwarding calls to other objects or classes. Ruby's Forwardable module works with singleton classes to create clean delegation interfaces.

require 'forwardable'

class UserService
  extend Forwardable

  class << self
    def_delegators :repository, :find, :create, :update

    private

    def repository
      @repository ||= UserRepository.new
    end
  end
end

# Delegates to UserRepository instance
UserService.find(123)
UserService.create(name: 'Alice')

Module inclusion affects class methods through the included hook, which can extend the including class with class methods. This pattern enables sharing class-level functionality across multiple classes.

module Timestampable
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def with_timestamps
      attr_accessor :created_at, :updated_at

      define_method :initialize do |*args|
        super(*args)
        @created_at = @updated_at = Time.current
      end
    end
  end
end

class Post
  include Timestampable
  with_timestamps
end

Common Pitfalls

Class methods defined with def self.method_name create singleton methods on the specific class object, not on the Class class itself. This distinction matters when working with inheritance and method lookup.

class Parent
  def self.class_method
    "parent"
  end
end

class Child < Parent
end

# This works - singleton class inheritance
Child.class_method
# => "parent"

# But this doesn't add methods to all classes
Class.new.class_method
# => NoMethodError

Private class methods require careful syntax consideration. Using private after def self.method_name doesn't make the method private. You need private_class_method or define methods within class << self with private.

class Example
  def self.public_method
    private_helper
  end

  def self.private_helper  # NOT PRIVATE
    "helper"
  end

  private  # This doesn't affect class methods above

  # Correct approaches:
  private_class_method :private_helper

  class << self
    private

    def another_private_method
      "truly private"
    end
  end
end

Example.private_helper      # Works - method is public
Example.another_private_method  # NoMethodError

Variable scoping in class methods differs from instance methods. Class methods cannot access instance variables directly, and local variables defined in class methods don't persist across method calls.

class Counter
  @count = 0  # Class instance variable

  def self.increment
    @count += 1  # Accessing class instance variable
  end

  def self.current
    @count
  end

  def increment_instance
    @count += 1  # Different @count - instance variable
  end
end

Counter.increment
Counter.current  # => 1

counter = Counter.new
counter.increment_instance
Counter.current  # => 1 (unchanged)

Method lookup for class methods follows the singleton class chain, which can lead to unexpected behavior when modules are involved. The singleton class of a class that includes a module doesn't automatically include the module's singleton methods.

module Trackable
  def self.setup
    "module setup"
  end

  def track
    "tracking instance"
  end
end

class Document
  include Trackable
end

Document.new.track     # Works - instance method
# Document.setup       # NoMethodError - module singleton method not inherited

# Correct way to share class methods:
module Trackable
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def setup
      "class setup"
    end
  end
end

Redefining class methods can lead to confusion about which method gets called, especially when using different definition syntaxes. The last definition wins, regardless of syntax used.

class Service
  def self.process
    "first definition"
  end

  class << self
    def process  # Overwrites previous definition
      "second definition"
    end
  end
end

Service.process  # => "second definition"

Reference

Class Method Definition Syntaxes

Syntax Usage Scope Access
def self.method_name Single method definition Class body
class << self; def method_name; end; end Multiple methods Singleton class
define_singleton_method(:name) Dynamic definition Runtime
def ClassName.method_name Outside class body Global

Singleton Class Access Methods

Method Returns Description
singleton_class Class Returns the singleton class
singleton_methods Array List of singleton methods
singleton_method(:name) Method Returns singleton method object
respond_to?(:method, true) Boolean Checks for method including private

Method Visibility Controls

Method Scope Usage
private_class_method :name Class body Make class method private
public_class_method :name Class body Make class method public
class << self; private; def name Singleton class Define private method
singleton_class.private :name Runtime Change visibility

Inheritance Behavior

Scenario Result Explanation
Child < Parent class methods Inherited Singleton class inheritance
Module singleton methods Not inherited Module's singleton class separate
extend Module Methods added Module methods become class methods
include Module with hook Methods added via extend Using included callback

Common Method Patterns

# Factory method pattern
def self.create_from_hash(data)
  new(data)
end

# Query methods
def self.active
  where(active: true)
end

# Configuration methods
def self.configure
  yield(configuration)
end

# Delegation pattern
def self.method_missing(method, *args, &block)
  if delegate_target.respond_to?(method)
    delegate_target.send(method, *args, &block)
  else
    super
  end
end

Error Types

Error Cause Solution
NoMethodError Method not defined Check method name and visibility
ArgumentError Wrong number of arguments Verify method signature
NameError Undefined local variable Use instance variables or parameters
TypeError Wrong object type Check class inheritance chain

Debugging Commands

# Inspect singleton methods
ClassName.singleton_methods
ClassName.singleton_methods(false)  # Exclude inherited

# Check method source location
ClassName.method(:method_name).source_location

# Inspect singleton class
ClassName.singleton_class
ClassName.singleton_class.ancestors

# Method lookup chain
ClassName.ancestors
ClassName.singleton_class.ancestors