CrackedRuby logo

CrackedRuby

Dynamic Attribute Methods

Overview

Dynamic attribute methods in Ruby provide mechanisms for creating and managing object attributes at runtime rather than compile time. Ruby accomplishes this through several core language features: method_missing, define_method, send, respond_to?, and the attribute accessor family (attr_reader, attr_writer, attr_accessor).

The foundation rests on Ruby's open class system and method lookup chain. When Ruby cannot find a method during normal lookup, it calls method_missing with the method name and arguments. This hook allows objects to respond to undefined methods dynamically.

Ruby's Module#define_method creates methods programmatically by accepting a method name and block. Unlike def, which defines methods at parse time, define_method creates methods during execution.

class DynamicUser
  def initialize(attributes = {})
    @attributes = attributes
  end
  
  def method_missing(method_name, *args, &block)
    if method_name.to_s.end_with?('=')
      attribute = method_name.to_s.chomp('=').to_sym
      @attributes[attribute] = args.first
    elsif @attributes.key?(method_name.to_sym)
      @attributes[method_name.to_sym]
    else
      super
    end
  end
  
  def respond_to_missing?(method_name, include_private = false)
    method_name.to_s.end_with?('=') || 
    @attributes.key?(method_name.to_sym) || 
    super
  end
end

user = DynamicUser.new(name: 'Alice', age: 30)
user.name          # => "Alice"
user.email = 'alice@example.com'
user.email         # => "alice@example.com"

The send method bypasses normal method dispatch, calling methods by name. This enables dynamic method invocation based on runtime data.

class Product
  attr_reader :name, :price, :category
  
  def initialize(name, price, category)
    @name, @price, @category = name, price, category
  end
end

product = Product.new('Laptop', 999.99, 'Electronics')
attributes = [:name, :price, :category]

# Dynamic attribute access
attributes.each do |attr|
  puts "#{attr}: #{product.send(attr)}"
end
# => name: Laptop
# => price: 999.99
# => category: Electronics

Ruby's standard library uses dynamic attribute methods extensively. The Struct class generates accessor methods at creation time. ActiveRecord and other ORMs implement dynamic finders and attribute methods using these patterns.

Basic Usage

Dynamic attribute creation typically starts with attr_reader, attr_writer, and attr_accessor. These methods generate getter and setter methods from symbols.

class BaseUser
  # Creates name and name= methods
  attr_accessor :name
  # Creates age method only
  attr_reader :age
  # Creates email= method only
  attr_writer :email
  
  def initialize(name, age)
    @name = name
    @age = age
  end
end

user = BaseUser.new('Bob', 25)
user.name           # => "Bob"
user.name = 'Bobby'
user.name           # => "Bobby"

For runtime attribute definition, define_method creates methods from data:

class ConfigurableClass
  ATTRIBUTES = [:title, :description, :status, :priority].freeze
  
  ATTRIBUTES.each do |attribute|
    define_method(attribute) do
      instance_variable_get("@#{attribute}")
    end
    
    define_method("#{attribute}=") do |value|
      instance_variable_set("@#{attribute}", value)
    end
  end
end

item = ConfigurableClass.new
item.title = 'Important Task'
item.status = 'pending'
puts item.title    # => "Important Task"
puts item.status   # => "pending"

The send method enables dynamic method calls when method names come from variables:

class DataProcessor
  attr_accessor :name, :value, :type
  
  def initialize(data)
    data.each do |key, val|
      send("#{key}=", val) if respond_to?("#{key}=")
    end
  end
  
  def get_attribute(attr_name)
    send(attr_name) if respond_to?(attr_name)
  end
end

processor = DataProcessor.new(name: 'Sample', value: 42, type: 'integer')
puts processor.get_attribute(:name)   # => "Sample"
puts processor.get_attribute(:value)  # => 42

Method existence checking prevents errors when calling dynamic methods:

class SafeAttributeReader
  def initialize(attributes)
    @attributes = attributes
  end
  
  def read_attribute(name)
    method_name = "get_#{name}"
    if respond_to?(method_name, true)
      send(method_name)
    else
      @attributes[name.to_sym]
    end
  end
  
  private
  
  def get_special_value
    'This is special'
  end
end

reader = SafeAttributeReader.new(normal_value: 'Regular data')
puts reader.read_attribute(:special_value)  # => "This is special"
puts reader.read_attribute(:normal_value)   # => "Regular data"

Advanced Usage

Complex dynamic attribute systems often combine multiple metaprogramming techniques. Method delegation with method_missing provides transparent attribute forwarding:

class AttributeProxy
  def initialize(target)
    @target = target
    @intercepted_methods = {}
  end
  
  def intercept(method_name, &block)
    @intercepted_methods[method_name.to_sym] = block
  end
  
  def method_missing(method_name, *args, &block)
    if @intercepted_methods.key?(method_name.to_sym)
      @intercepted_methods[method_name.to_sym].call(*args, &block)
    elsif @target.respond_to?(method_name)
      result = @target.send(method_name, *args, &block)
      log_access(method_name, args, result)
      result
    else
      super
    end
  end
  
  def respond_to_missing?(method_name, include_private = false)
    @intercepted_methods.key?(method_name.to_sym) || 
    @target.respond_to?(method_name, include_private) || 
    super
  end
  
  private
  
  def log_access(method, args, result)
    puts "Accessed #{method} with #{args.inspect} -> #{result.inspect}"
  end
end

class User
  attr_accessor :name, :email, :role
  
  def initialize(name, email, role)
    @name, @email, @role = name, email, role
  end
end

user = User.new('Charlie', 'charlie@example.com', 'admin')
proxy = AttributeProxy.new(user)

# Add custom behavior for specific methods
proxy.intercept(:role) { |*args| 
  args.empty? ? user.role.upcase : user.role = args.first.downcase 
}

puts proxy.name         # => "Charlie" (with log output)
puts proxy.role         # => "ADMIN"
proxy.role = 'USER'
puts user.role          # => "user"

Dynamic class modification enables attribute injection at runtime:

module DynamicAttributes
  def self.included(base)
    base.extend(ClassMethods)
  end
  
  module ClassMethods
    def add_attributes(*attribute_names)
      attribute_names.each do |name|
        create_attribute_methods(name)
      end
    end
    
    private
    
    def create_attribute_methods(name)
      define_method(name) do
        instance_variable_get("@#{name}")
      end
      
      define_method("#{name}=") do |value|
        old_value = instance_variable_get("@#{name}")
        instance_variable_set("@#{name}", value)
        attribute_changed(name, old_value, value) if respond_to?(:attribute_changed, true)
      end
      
      define_method("#{name}?") do
        value = instance_variable_get("@#{name}")
        value.respond_to?(:empty?) ? !value.empty? : !!value
      end
    end
  end
  
  private
  
  def attribute_changed(name, old_value, new_value)
    puts "#{name} changed from #{old_value.inspect} to #{new_value.inspect}"
  end
end

class Product
  include DynamicAttributes
  
  def initialize
    @changed_attributes = {}
  end
end

Product.add_attributes(:name, :price, :description)

product = Product.new
product.name = 'Widget'        # => "name changed from nil to \"Widget\""
product.price = 29.99          # => "price changed from nil to 29.99"
puts product.name?             # => true

Chainable attribute builders create fluent interfaces:

class FluentBuilder
  def initialize
    @attributes = {}
  end
  
  def method_missing(method_name, *args, &block)
    if args.length == 1
      @attributes[method_name] = args.first
      self
    elsif args.empty?
      @attributes[method_name]
    else
      super
    end
  end
  
  def respond_to_missing?(method_name, include_private = false)
    true
  end
  
  def build
    result = Object.new
    @attributes.each do |name, value|
      result.define_singleton_method(name) { value }
    end
    result
  end
  
  def to_h
    @attributes.dup
  end
end

config = FluentBuilder.new
  .database_url('postgresql://localhost/mydb')
  .redis_url('redis://localhost:6379')
  .log_level(:debug)
  .timeout(30)

puts config.to_h
# => {:database_url=>"postgresql://localhost/mydb", :redis_url=>"redis://localhost:6379", :log_level=>:debug, :timeout=>30}

built_config = config.build
puts built_config.database_url  # => "postgresql://localhost/mydb"
puts built_config.timeout       # => 30

Common Pitfalls

Dynamic attribute methods introduce several error-prone patterns. Method naming conflicts occur when dynamic methods shadow existing methods:

# Problematic: shadows Object methods
class BadDynamicClass
  def method_missing(method_name, *args)
    if method_name.to_s.end_with?('=')
      instance_variable_set("@#{method_name.to_s.chomp('=')}", args.first)
    else
      instance_variable_get("@#{method_name}")
    end
  end
end

obj = BadDynamicClass.new
obj.class = 'MyClass'  # Shadows Object#class!
puts obj.class         # Returns string instead of Class object

# Better: namespace dynamic attributes or check for conflicts
class SafeDynamicClass
  RESERVED_METHODS = (Object.instance_methods + 
                     BasicObject.instance_methods).map(&:to_s).freeze
  
  def method_missing(method_name, *args)
    method_str = method_name.to_s
    
    if RESERVED_METHODS.include?(method_str) || 
       RESERVED_METHODS.include?(method_str.chomp('='))
      super
    elsif method_str.end_with?('=')
      attr_name = method_str.chomp('=')
      instance_variable_set("@#{attr_name}", args.first)
    else
      instance_variable_get("@#{method_name}")
    end
  end
  
  def respond_to_missing?(method_name, include_private = false)
    method_str = method_name.to_s
    !(RESERVED_METHODS.include?(method_str) || 
      RESERVED_METHODS.include?(method_str.chomp('='))) || super
  end
end

Performance degradation occurs with excessive method_missing usage since it bypasses Ruby's method cache:

# Slow: method_missing called every time
class SlowAttributes
  def initialize
    @data = {}
  end
  
  def method_missing(method_name, *args)
    @data[method_name] || super
  end
end

# Faster: define methods after first access
class CachedAttributes
  def initialize
    @data = {}
  end
  
  def method_missing(method_name, *args)
    if @data.key?(method_name)
      # Define the method for future calls
      self.class.define_method(method_name) do
        @data[method_name]
      end
      
      @data[method_name]
    else
      super
    end
  end
end

# Best: pre-define known attributes
class PreDefinedAttributes
  def self.attribute(name, default_value = nil)
    define_method(name) do
      instance_variable_get("@#{name}") || default_value
    end
    
    define_method("#{name}=") do |value|
      instance_variable_set("@#{name}", value)
    end
  end
end

Missing respond_to_missing? implementation breaks method introspection:

# Broken: respond_to? returns false for dynamic methods
class BrokenIntrospection
  def method_missing(method_name, *args)
    "Dynamic: #{method_name}"
  end
end

obj = BrokenIntrospection.new
obj.anything           # => "Dynamic: anything"
obj.respond_to?(:anything)  # => false (incorrect!)

# Fixed: implement respond_to_missing?
class FixedIntrospection
  def method_missing(method_name, *args)
    "Dynamic: #{method_name}"
  end
  
  def respond_to_missing?(method_name, include_private = false)
    true  # All method names are supported
  end
end

obj = FixedIntrospection.new
obj.anything           # => "Dynamic: anything"
obj.respond_to?(:anything)  # => true (correct!)

Reference

Core Methods

Method Parameters Returns Description
method_missing(name, *args, &block) name (Symbol), *args (Array), &block (Proc) Object Called when method lookup fails
respond_to_missing?(name, include_private) name (Symbol), include_private (Boolean) Boolean Checks if method_missing handles method
define_method(name, method) name (Symbol/String), method (Method/Proc/Block) Symbol Defines instance method dynamically
send(name, *args, &block) name (Symbol/String), *args (Array), &block (Proc) Object Calls method by name
public_send(name, *args, &block) name (Symbol/String), *args (Array), &block (Proc) Object Calls public method by name
respond_to?(name, include_all) name (Symbol/String), include_all (Boolean) Boolean Checks method availability

Attribute Definition Methods

Method Parameters Returns Description
attr_reader(*names) *names (Symbol/String) nil Creates getter methods
attr_writer(*names) *names (Symbol/String) nil Creates setter methods
attr_accessor(*names) *names (Symbol/String) nil Creates getter and setter methods
instance_variable_get(name) name (Symbol/String) Object Gets instance variable value
instance_variable_set(name, value) name (Symbol/String), value (Object) Object Sets instance variable value
instance_variable_defined?(name) name (Symbol/String) Boolean Checks instance variable existence

Method Introspection

Method Parameters Returns Description
methods(regular) regular (Boolean) Array<Symbol> Lists available methods
private_methods(all) all (Boolean) Array<Symbol> Lists private methods
public_methods(all) all (Boolean) Array<Symbol> Lists public methods
method(name) name (Symbol/String) Method Gets Method object
instance_method(name) name (Symbol/String) UnboundMethod Gets UnboundMethod object

Common Patterns

Pattern Implementation Use Case
Dynamic Getters define_method(name) { @#{name} } Runtime attribute creation
Dynamic Setters define_method("#{name}=") { |v| @#{name} = v } Runtime attribute creation
Attribute Forwarding method_missing + send to target Proxy objects
Method Caching method_missing + define_method Performance optimization
Fluent Interface method_missing returning self Builder pattern
Safe Dynamic Calls send with respond_to? check Runtime method dispatch

Error Types

Exception Cause Solution
NoMethodError Method not found and no method_missing Implement method_missing or define method
ArgumentError Wrong number of arguments Check method signatures
NameError Invalid method name Validate method names before creation
SystemStackError Infinite method_missing recursion Add termination conditions

Performance Characteristics

Technique Speed Memory Caching Best For
Pre-defined methods Fastest Low Full Known attributes
define_method Fast Medium Full Runtime generation
method_missing Slow Low None Truly dynamic behavior
Cached method_missing Medium Medium Partial Mixed scenarios

Reserved Method Names

Methods that should not be overridden dynamically:

  • Object methods: class, object_id, instance_of?, kind_of?, nil?, respond_to?, send, public_send
  • BasicObject methods: equal?, !, !=, __id__, __send__
  • Kernel methods: puts, print, p, require, load, eval, exec, system