CrackedRuby logo

CrackedRuby

Method Definition Styles Comparison

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