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