Ruby Metaprogramming — define_method send and Dynamic Code Generation Explained
In this tutorial, you will learn about Ruby Metaprogramming. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby Metaprogramming enables runtime code generation using define_method for dynamic method creation, send for dynamic dispatch, method_missing for ghost methods, and eval for executing arbitrary strings as code.
What You'll Learn
- Creating methods dynamically
- Using send for dynamic dispatch
- Runtime class manipulation
- Building DSLs with metaprogramming
Why It Matters
Metaprogramming is Ruby's superpower. Durga Antivirus Pro uses metaprogramming for plugin systems and rule engines. Doda Browser uses it for configuration DSLs and dynamic form builders. Rails itself is built on metaprogramming — every belongs_to, validates, and scope call uses it.
Real-World Use
Rails' validations, associations, and scopes are all metaprogramming. Gems like Devise, CanCanCan, and PaperTrail use it extensively. ORMs generate methods based on schema.
flowchart LR
A["Metaprogramming"] --> B["define_method"]
B --> C["send"]
C --> D["method_missing"]
D --> E["DSL"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
define_method
Creates methods dynamically at runtime:
class DynamicMethods
[:name, :email, :phone].each do |field|
define_method("get_#{field}") do
instance_variable_get("@#{field}")
end
define_method("set_#{field}") do |value|
instance_variable_set("@#{field}", value)
end
end
end
obj = DynamicMethods.new
obj.set_name("Alice")
obj.set_email("alice@example.com")
puts obj.get_name # Alice
puts obj.get_email # alice@example.com
Bulk Method Definition
class ApiClient
%w[get post put patch delete].each do |http_method|
define_method(http_method) do |path, **params|
request(method: http_method.upcase, path: path, body: params)
end
end
private
def request(method:, path:, body: {})
puts "#{method} #{path} with #{body.inspect}"
end
end
client = ApiClient.new
client.get("/users")
client.post("/users", name: "Alice")
client.delete("/users/1")
# GET /users with {}
# POST /users with {:name=>"Alice"}
# DELETE /users/1 with {}
send
Calls methods by name (string or symbol):
class Calculator
def add(a, b) = a + b
def subtract(a, b) = a - b
def multiply(a, b) = a * b
def divide(a, b) = a / b
end
calc = Calculator.new
operation = :add
puts calc.send(operation, 10, 5) # 15
# Dynamic dispatch
["add", "subtract", "multiply"].each do |op|
result = calc.send(op, 10, 5)
puts "#{op}: #{result}"
end
# add: 15
# subtract: 5
# multiply: 50
send vs public_send
class SecureClass
def public_method
"public"
end
private
def secret_method
"secret"
end
end
obj = SecureClass.new
puts obj.send(:public_method) # public
puts obj.send(:secret_method) # secret (bypasses privacy!)
# puts obj.public_send(:secret_method) # NoMethodError
respond_to? and method_missing
class DynamicConfig
def initialize
@data = {}
end
def method_missing(name, *args, &block)
key = name.to_s
if key.end_with?("=")
@data[key.chop] = args.first
elsif @data.key?(key)
@data[key]
else
super
end
end
def respond_to_missing?(name, include_private = false)
key = name.to_s
@data.key?(key) || key.end_with?("=") || super
end
end
config = DynamicConfig.new
config.api_key = "secret-123"
config.timeout = 30
puts config.api_key # secret-123
puts config.timeout # 30
puts config.respond_to?(:api_key) # true
class_eval and instance_eval
# class_eval — adds methods to class
String.class_eval do
def dash_case
tr("_", "-")
end
end
puts "hello_world".dash_case # hello-world
# instance_eval — runs in context of instance
obj = "hello"
obj.instance_eval do
def shout
"#{self.upcase}!"
end
end
puts obj.shout # HELLO!
# class_eval with define_method
class User
ATTRIBUTES = %i[name email role]
ATTRIBUTES.each do |attr|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{attr}
@#{attr}
end
def #{attr}=(value)
@#{attr} = value
end
RUBY
end
end
user = User.new
user.name = "Alice"
puts user.name # Alice
const_get and const_set
class NotificationFactory
TYPES = { email: EmailNotification, sms: SMSNotification, push: PushNotification }
def self.create(type, *args)
klass = const_get(:"#{type.to_s.capitalize}Notification")
klass.new(*args)
rescue NameError
raise "Unknown notification type: #{type}"
end
end
class EmailNotification
def send(msg) = puts "Email: #{msg}"
end
class SMSNotification
def send(msg) = puts "SMS: #{msg}"
end
notif = NotificationFactory.create(:email, "alice@example.com")
notif.send("Hello!") # Email: Hello!
Building a DSL
class Router
class << self
def routes
@routes ||= []
end
def get(path, to:)
routes << { method: :GET, path: path, handler: to }
end
def post(path, to:)
routes << { method: :POST, path: path, handler: to }
end
def draw(&block)
instance_exec(&block)
end
end
end
# DSL usage
Router.draw do
get "/users", to: "users#index"
get "/users/:id", to: "users#show"
post "/users", to: "users#create"
end
Router.routes.each { |r| puts "#{r[:method]} #{r[:path]} -> #{r[:handler]}" }
# GET /users -> users#index
# GET /users/:id -> users#show
# POST /users -> users#create
ActiveRecord-style Macro
class BaseModel
class << self
def attributes(*names)
names.each do |name|
define_method(name) { instance_variable_get("@#{name}") }
define_method("#{name}=") { |v| instance_variable_set("@#{name}", v) }
end
end
def has_many(name, options = {})
define_method(name) do
klass = options[:class_name]&.constantize || name.to_s.classify.constantize
klass.where("#{self.class.name.underscore}_id" => id)
end
end
def validates(name, **options)
@validations ||= []
@validations << [name, options]
end
end
def save
self.class.instance_variable_get(:@validations)&.each do |name, options|
value = send(name)
if options[:presence] && (value.nil? || value.empty?)
raise "Validation failed: #{name} can't be blank"
end
end
true
end
end
class Product < BaseModel
attributes :name, :price, :sku
validates :name, presence: true
validates :sku, presence: true
end
product = Product.new
product.name = "Widget"
product.price = 9.99
puts product.save # true
Common Mistakes
1. Eval with User Input
# Dangerous — code injection
def run_user_code(code)
eval(code) # Never do this with user input!
end
# Safe — use send or public_send
def call_method(obj, method_name, *args)
obj.public_send(method_name, *args)
end
2. Forgetting respond_to_missing?
class Proxy
def method_missing(name, *args)
"handled #{name}"
end
end
p = Proxy.new
p.respond_to?(:anything) # false — breaks duck typing!
3. Performance of define_method in Hot Paths
# define_method in a loop called frequently
# Better to define statically or cache
# OK for setup-time (class definition)
# Bad if called repeatedly at runtime
4. Overusing Metaprogramming
# Unnecessary complexity — simple code is better
class Point
[:x, :y].each { |a| attr_accessor a }
# Just use: attr_accessor :x, :y
end
5. Method Collisions with method_missing
class HashConfig
def method_missing(name, *args)
if @data.key?(name.to_s)
@data[name.to_s]
else
super
end
end
# Now you can't call any real methods!
# c.object_id returns @data["object_id"], not the real object_id
end
Practice Questions
1. What does define_method do?
Creates a method at runtime with the given name and block. define_method(:greet) { "Hello" } creates a greet method. Useful for creating families of methods dynamically.
2. What's the difference between send and public_send?
send can call any method including private/protected. public_send respects method visibility and only calls public methods. Use public_send for safer dynamic dispatch.
3. What is a DSL in Ruby?
A Domain-Specific Language that uses Ruby syntax to Express concepts naturally. Examples: validates :name, presence: true, scope :active, -> { where(active: true) }.
4. Why should you avoid eval with user input?
eval executes arbitrary Ruby code. User-provided strings can contain malicious code like system("rm -rf /"). Use safe alternatives like send or whitelist-based dispatch.
Challenge: Build a simple ActiveRecord-like query Builder using metaprogramming that supports where chaining, lazy evaluation, and dynamic attribute accessors.
Solution
class QueryBuilder
def initialize(klass)
@klass = klass
@conditions = []
end
def where(**conditions)
@conditions << conditions
self
end
def method_missing(name, *args)
if name.to_s.start_with?("find_by_")
attrs = name.to_s.sub("find_by_", "").split("_and_")
conditions = attrs.zip(args).to_h
where(**conditions).first
elsif @klass.instance_methods.include?(name)
raise "Use execute instead"
else
super
end
end
def respond_to_missing?(name, include_private = false)
name.to_s.start_with?("find_by_") || super
end
def execute
sql = "SELECT * FROM #{@klass.name.downcase}s"
unless @conditions.empty?
where_clauses = @conditions.flat_map { |c| c.map { |k, v| "#{k} = '#{v}'" } }
sql += " WHERE #{where_clauses.join(" AND ")}"
end
puts sql
# In real code: ActiveRecord::Base.connection.execute(sql)
end
end
class Model
def self.query
QueryBuilder.new(self)
end
end
class User < Model; end
# Usage
User.query.where(name: "Alice", active: true).execute
# SELECT * FROM users WHERE name = 'Alice' AND active = true
User.query.find_by_email("alice@example.com")
# SELECT * FROM users WHERE email = 'alice@example.com'
FAQ
{{< faq question="Is metaprogramming slow?" >}}
define_method is fast (only slightly slower than regular methods). send has minimal overhead. method_missing is slower because it requires a failed method lookup first. eval is slowest and should be avoided.
{{< /faq >}}
{{< faq question="Should I use metaprogramming in production code?" >}} Yes, but sparingly. Rails uses it extensively. The rule: use metaprogramming when it reduces repetition significantly and the pattern is obvious. Avoid it when simple code suffices. {{< /faq >}}
{{< faq question="How do I debug metaprogramming code?" >}}
Use obj.methods, obj.instance_methods, obj.private_methods, and obj.method(:name).source_location to inspect dynamically created methods. Method#parameters shows parameter info.
{{< /faq >}}
{{< faq question="What is the difference between class_eval and instance_eval?" >}}
class_eval runs in the context of a class, defining instance methods. instance_eval runs in the context of a specific instance, defining Singleton methods.
{{< /faq >}}
{{< faq question="Can I undefine a dynamically created method?" >}}
Yes. Use remove_method(:method_name) to remove from the class, or undef_method(:method_name) to prevent it from being called via inheritance.
{{< /faq >}}
Try It Yourself
# metaprogramming_demo.rb
class AttributeBuilder
def self.build_attributes(*names)
names.each do |name|
define_method(name) { instance_variable_get("@#{name}") }
define_method("#{name}=") { |value| instance_variable_set("@#{name}", value) }
define_method("#{name}?") { !!send(name) }
end
end
end
class Task < AttributeBuilder
build_attributes :title, :completed, :priority
def initialize(title = nil, completed: false, priority: :medium)
@title = title
@completed = completed
@priority = priority
end
def to_s
status = completed? ? "[x]" : "[ ]"
"#{status} #{title} (priority: #{priority})"
end
end
task = Task.new("Learn metaprogramming", completed: false, priority: :high)
puts task.title # Learn metaprogramming
puts task.completed? # false
puts task.to_s # [ ] Learn metaprogramming (priority: high)
task.completed = true
puts task.completed? # true
puts task.to_s # [x] Learn metaprogramming (priority: high)
What's Next
Now that you understand metaprogramming basics, learn how to build Domain-Specific Languages (DSLs) in Ruby.
| Topic | Description | Link |
|---|---|---|
| Ruby DSL | Creating fluent interfaces | {{< ref "34-dsl" >}} |
| Ruby send/define_method | Dynamic method calls | {{< ref "35-send-define-method" >}} |
| Python Metaclasses | Compare Python's metaprogramming | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro