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