Overview
Ruby provides multiple approaches for defining methods, each with distinct characteristics regarding scope, performance, and metaprogramming capabilities. The standard def
keyword creates methods at parse time, while dynamic alternatives like define_method
generate methods at runtime. Method definition styles affect visibility rules, closure behavior, and debugging capabilities differently.
The core method definition mechanisms include explicit definition with def
, dynamic creation through define_method
, singleton method definition, and method aliasing. Each approach interacts uniquely with Ruby's object model, affecting how methods bind to objects, capture lexical scope, and handle inheritance.
class Example
# Standard definition - parsed at class definition time
def standard_method
"defined with def"
end
# Dynamic definition - created at runtime
define_method(:dynamic_method) do
"defined with define_method"
end
# Singleton method - attached to specific instance
def self.class_method
"singleton method on class object"
end
end
Example.new.standard_method # => "defined with def"
Example.new.dynamic_method # => "defined with define_method"
Example.class_method # => "singleton method on class object"
Method definition timing impacts performance characteristics. Standard def
methods compile to bytecode during parsing, while define_method
generates methods during execution. This distinction affects both memory usage and method dispatch performance in high-frequency scenarios.
# Performance comparison setup
class MethodComparison
# Define 100 methods using def
100.times do |i|
define_method("def_method_#{i}") { "method #{i}" }
end
# Define 100 methods using define_method
100.times do |i|
eval "def eval_method_#{i}; 'method #{i}'; end"
end
end
Visibility modifiers interact differently with various definition styles. The private
, protected
, and public
keywords affect subsequent method definitions when using def
, but require explicit specification with define_method
and other dynamic approaches.
Basic Usage
Standard method definition with def
creates methods in the current scope's method table. The method name becomes a symbol key, and Ruby generates bytecode for the method body during class or module definition.
class StandardDefinition
def calculate(x, y)
x * y + 42
end
def greet(name = "World")
"Hello, #{name}!"
end
def process(*args, **kwargs)
{ args: args, kwargs: kwargs }
end
end
obj = StandardDefinition.new
obj.calculate(5, 3) # => 57
obj.greet # => "Hello, World!"
obj.greet("Ruby") # => "Hello, Ruby!"
obj.process(1, 2, key: "val") # => {:args=>[1, 2], :kwargs=>{:key=>"val"}}
Dynamic method definition through define_method
accepts a block that becomes the method body. This approach captures the lexical scope at definition time, creating true closures over local variables.
class DynamicDefinition
def self.create_accessor(name)
# Closure captures the name parameter
define_method(name) do
instance_variable_get("@#{name}")
end
define_method("#{name}=") do |value|
instance_variable_set("@#{name}", value)
end
end
create_accessor(:username)
create_accessor(:email)
end
user = DynamicDefinition.new
user.username = "john_doe"
user.email = "john@example.com"
user.username # => "john_doe"
user.email # => "john@example.com"
Singleton methods attach directly to specific objects rather than their classes. Ruby provides multiple syntaxes for singleton definition, including def object.method
and definition within singleton classes.
class SingletonExample
def instance_method
"shared by all instances"
end
end
obj1 = SingletonExample.new
obj2 = SingletonExample.new
# Singleton method on specific instance
def obj1.special_method
"only available on obj1"
end
# Alternative singleton syntax
class << obj2
def another_special_method
"only available on obj2"
end
end
obj1.instance_method # => "shared by all instances"
obj2.instance_method # => "shared by all instances"
obj1.special_method # => "only available on obj1"
obj2.another_special_method # => "only available on obj2"
obj2.respond_to?(:special_method) # => false
obj1.respond_to?(:another_special_method) # => false
Method aliasing creates alternative names for existing methods. Ruby provides both alias
and alias_method
, with subtle differences in evaluation timing and scope handling.
class AliasingExample
def original_method
"original implementation"
end
alias_method :copy_method, :original_method
alias alternative_method original_method
def original_method
"modified implementation"
end
end
obj = AliasingExample.new
obj.original_method # => "modified implementation"
obj.copy_method # => "original implementation"
obj.alternative_method # => "original implementation"
Advanced Usage
Metaprogramming with method definitions enables sophisticated patterns like method generation, delegation, and dynamic interface creation. The define_method
approach excels at programmatic method creation where method names or behaviors depend on runtime data.
class APIClient
ENDPOINTS = {
users: "/api/v1/users",
posts: "/api/v1/posts",
comments: "/api/v1/comments"
}.freeze
# Generate methods for each API endpoint
ENDPOINTS.each do |resource, path|
define_method("get_#{resource}") do |id = nil|
url = id ? "#{path}/#{id}" : path
make_request(:get, url)
end
define_method("create_#{resource}") do |data|
make_request(:post, path, data)
end
define_method("update_#{resource}") do |id, data|
make_request(:put, "#{path}/#{id}", data)
end
define_method("delete_#{resource}") do |id|
make_request(:delete, "#{path}/#{id}")
end
end
private
def make_request(method, url, data = nil)
"#{method.upcase} #{url}" + (data ? " with #{data}" : "")
end
end
client = APIClient.new
client.get_users # => "GET /api/v1/users"
client.create_posts(title: "New Post") # => "POST /api/v1/posts with {:title=>\"New Post\"}"
client.update_comments(5, body: "Updated") # => "PUT /api/v1/comments/5 with {:body=>\"Updated\"}"
Method composition through delegation and forwarding creates flexible object interfaces. Ruby's method definition styles support various delegation patterns, from simple forwarding to complex behavior modification.
class DatabaseConnection
def initialize(config)
@config = config
@connected = false
end
def query(sql)
"Executing: #{sql}"
end
def connect
@connected = true
"Connected to #{@config[:database]}"
end
end
class Repository
def initialize(connection)
@connection = connection
create_delegated_methods
create_logging_wrappers
end
private
def create_delegated_methods
[:connect].each do |method_name|
define_method(method_name) do |*args, **kwargs|
@connection.public_send(method_name, *args, **kwargs)
end
end
end
def create_logging_wrappers
[:query].each do |method_name|
# Store original method reference
original_method = @connection.method(method_name)
define_method(method_name) do |*args, **kwargs|
puts "Logging: #{method_name} called with #{args.inspect}"
result = original_method.call(*args, **kwargs)
puts "Logging: #{method_name} returned #{result.inspect}"
result
end
end
end
end
repo = Repository.new(DatabaseConnection.new(database: "myapp_production"))
repo.connect # => "Connected to myapp_production"
repo.query("SELECT * FROM users")
# Output:
# Logging: query called with ["SELECT * FROM users"]
# Logging: query returned "Executing: SELECT * FROM users"
Module inclusion and method definition interaction creates complex inheritance hierarchies. Understanding how different definition styles interact with module inclusion helps design extensible architectures.
module Trackable
def self.included(base)
base.extend(ClassMethods)
base.include(InstanceMethods)
end
module ClassMethods
def track_method(method_name)
alias_method "#{method_name}_without_tracking", method_name
define_method(method_name) do |*args, **kwargs|
puts "Tracking call to #{method_name}"
result = public_send("#{method_name}_without_tracking", *args, **kwargs)
puts "Finished call to #{method_name}"
result
end
end
end
module InstanceMethods
def tracking_enabled?
true
end
end
end
class BusinessLogic
include Trackable
def process_data(data)
"Processing #{data.size} items"
end
def calculate_metrics(dataset)
"Calculated metrics for #{dataset}"
end
# Enable tracking for specific methods
track_method :process_data
track_method :calculate_metrics
end
logic = BusinessLogic.new
logic.process_data([1, 2, 3])
# Output:
# Tracking call to process_data
# Finished call to process_data
Method missing and dynamic method creation enables proxy objects and flexible APIs. This pattern requires careful consideration of method definition timing and scope management.
class DynamicProxy
def initialize(target)
@target = target
@method_cache = {}
end
def method_missing(method_name, *args, **kwargs)
if @target.respond_to?(method_name)
create_proxy_method(method_name)
public_send(method_name, *args, **kwargs)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
@target.respond_to?(method_name, include_private) || super
end
private
def create_proxy_method(method_name)
return if @method_cache[method_name]
@method_cache[method_name] = true
define_singleton_method(method_name) do |*args, **kwargs|
puts "Proxying #{method_name} to target object"
@target.public_send(method_name, *args, **kwargs)
end
end
end
target = "Hello World"
proxy = DynamicProxy.new(target)
proxy.upcase # Creates and calls upcase method
proxy.downcase # Creates and calls downcase method
proxy.length # Creates and calls length method
Common Pitfalls
Method visibility modifiers behave inconsistently across definition styles. The private
, protected
, and public
keywords affect subsequent def
statements but require explicit specification with define_method
and other dynamic approaches.
class VisibilityPitfalls
def public_method
"accessible everywhere"
end
private
def private_def_method
"private via def"
end
# This method is NOT private - define_method ignores previous visibility
define_method(:not_actually_private) do
"this is public despite private keyword above"
end
# Correct way to make define_method private
define_method(:correctly_private) do
"this is properly private"
end
private :correctly_private
public
def test_access
private_def_method # Works - calling from within class
not_actually_private # Works - method is public
correctly_private # Works - calling from within class
end
end
obj = VisibilityPitfalls.new
obj.public_method # => "accessible everywhere"
obj.not_actually_private # => "this is public despite private keyword above"
begin
obj.private_def_method
rescue NoMethodError => e
puts e.message # => private method `private_def_method' called
end
begin
obj.correctly_private
rescue NoMethodError => e
puts e.message # => private method `correctly_private' called
end
Closure behavior differs between def
and define_method
, leading to unexpected variable capture. Methods defined with def
cannot access local variables from the surrounding scope, while define_method
creates true closures.
class ClosurePitfalls
def self.demonstrate_closure_differences
local_variable = "captured value"
# This will NOT work - def cannot access local variables
def regular_method
local_variable # NameError: undefined local variable
end
# This WILL work - define_method creates closure
define_method(:closure_method) do
local_variable # Accesses the local variable
end
# Proof that the local variable exists
puts "Local variable: #{local_variable}"
end
# Demonstrate at class level
counter = 0
define_method(:increment_counter) do
counter += 1
end
define_method(:get_counter) do
counter
end
# This would fail:
# def broken_counter
# counter # NameError
# end
end
# ClosurePitfalls.demonstrate_closure_differences
obj = ClosurePitfalls.new
obj.increment_counter # => 1
obj.increment_counter # => 2
obj.get_counter # => 2
Method aliasing timing creates subtle bugs when aliases are created before method redefinition. The alias captures the current implementation, not the final one.
class AliasingPitfall
def original_method
"first implementation"
end
# Alias captures current implementation
alias_method :early_alias, :original_method
def original_method
"second implementation"
end
# Alias captures the redefined implementation
alias_method :late_alias, :original_method
def demonstrate_difference
puts "original_method: #{original_method}"
puts "early_alias: #{early_alias}"
puts "late_alias: #{late_alias}"
end
end
obj = AliasingPitfall.new
obj.demonstrate_difference
# Output:
# original_method: second implementation
# early_alias: first implementation
# late_alias: second implementation
Singleton method definition on literals creates confusion because literals may be frozen or shared. Ruby handles singleton methods differently on different object types.
class SingletonPitfall
def self.demonstrate_literal_issues
# This works on mutable objects
str = "mutable string"
def str.custom_method
"added to string"
end
puts str.custom_method # => "added to string"
# This fails on frozen literals
begin
frozen_str = "frozen string".freeze
def frozen_str.custom_method # FrozenError
"won't work"
end
rescue FrozenError => e
puts "Frozen string error: #{e.message}"
end
# This fails on numeric literals
begin
def 42.custom_method # SyntaxError or TypeError
"won't work on number"
end
rescue => e
puts "Number error: #{e.class}: #{e.message}"
end
# Symbol behavior varies by Ruby version
symbol = :test_symbol
begin
def symbol.custom_method
"added to symbol"
end
puts "Symbol method worked: #{symbol.custom_method}"
rescue => e
puts "Symbol error: #{e.class}: #{e.message}"
end
end
end
# SingletonPitfall.demonstrate_literal_issues
Method definition in class versus instance context causes confusion about where methods are defined. Understanding the current self
value determines method placement.
class ContextConfusion
puts "During class definition, self is: #{self}"
def self.class_method
puts "Class method, self is: #{self}"
end
def instance_method
puts "Instance method, self is: #{self}"
# Defining method inside instance method creates singleton method
def singleton_from_instance
"singleton method created from instance context"
end
end
# This creates an instance method despite being in class context
define_method(:dynamic_instance_method) do
puts "Dynamic instance method, self is: #{self}"
end
class << self
puts "In singleton class, self is: #{self}"
def another_class_method
puts "Another class method, self is: #{self}"
end
end
end
obj = ContextConfusion.new
ContextConfusion.class_method
obj.instance_method
obj.dynamic_instance_method
ContextConfusion.another_class_method
# After calling instance_method, obj gains singleton method
obj.singleton_from_instance # => "singleton method created from instance context"
# Other instances don't have this method
obj2 = ContextConfusion.new
obj2.respond_to?(:singleton_from_instance) # => false
Reference
Method Definition Syntax
Syntax | Scope | Timing | Closure | Performance |
---|---|---|---|---|
def name; end |
Current class/module | Parse time | No | Fastest |
define_method(:name) {} |
Current class/module | Runtime | Yes | Slower |
def obj.name; end |
Singleton class | Parse time | No | Fast |
obj.define_singleton_method(:name) {} |
Singleton class | Runtime | Yes | Slower |
class << obj; def name; end; end |
Singleton class | Parse time | No | Fast |
Visibility Control Methods
Method | Scope | Usage | Example |
---|---|---|---|
private |
Subsequent methods | private |
All following methods become private |
protected |
Subsequent methods | protected |
All following methods become protected |
public |
Subsequent methods | public |
All following methods become public |
private :method |
Specific method | private :method_name |
Make specific method private |
protected :method |
Specific method | protected :method_name |
Make specific method protected |
public :method |
Specific method | public :method_name |
Make specific method public |
Aliasing Methods
Method | Evaluation | Scope | Syntax |
---|---|---|---|
alias |
Parse time | Current scope | alias new_name old_name |
alias_method |
Runtime | Current scope | alias_method :new_name, :old_name |
Method Introspection
Method | Returns | Description |
---|---|---|
#method(name) |
Method |
Method object for bound method |
#public_method(name) |
Method |
Method object for public method only |
.instance_method(name) |
UnboundMethod |
Unbound method from class |
#methods |
Array<Symbol> |
All available method names |
#public_methods |
Array<Symbol> |
Public method names only |
#private_methods |
Array<Symbol> |
Private method names only |
#protected_methods |
Array<Symbol> |
Protected method names only |
#singleton_methods |
Array<Symbol> |
Singleton method names |
#respond_to?(name) |
Boolean |
Whether object responds to method |
#respond_to_missing? |
Boolean |
Hook for dynamic method detection |
Common Patterns
# Dynamic accessor creation
define_method("#{name}=") { |value| instance_variable_set("@#{name}", value) }
define_method(name) { instance_variable_get("@#{name}") }
# Method delegation
define_method(method_name) { |*args, **kwargs| target.public_send(method_name, *args, **kwargs) }
# Conditional method definition
define_method(:feature_method) { "available" } if RUBY_VERSION >= "2.7"
# Method composition
alias_method "#{method_name}_without_feature", method_name
define_method(method_name) { |*args| feature_wrapper { public_send("#{method_name}_without_feature", *args) } }
# Singleton method creation
obj.define_singleton_method(:custom) { "specific to this instance" }
Performance Characteristics
Definition Style | Method Creation | Method Call | Memory Usage | Debug Info |
---|---|---|---|---|
def |
Fast | Fastest | Low | Complete |
define_method |
Slower | Fast | Higher | Limited |
eval "def" |
Slowest | Fastest | Highest | Poor |
Singleton methods | Fast | Fast | Medium | Complete |
Error Types
Error | Cause | Context |
---|---|---|
NoMethodError |
Method not defined or not visible | Method call |
ArgumentError |
Wrong number of parameters | Method call |
NameError |
Invalid method name or scope issue | Method definition |
FrozenError |
Attempt to modify frozen object | Singleton method definition |
SyntaxError |
Invalid method definition syntax | Parse time |