Dynamic class creation in Ruby allows programs to define classes at runtime using metaprogramming techniques and the Class.new constructor.
Overview
Dynamic class creation refers to defining classes programmatically during program execution rather than through static class declarations. Ruby provides multiple mechanisms for runtime class generation, with Class.new
serving as the primary constructor for creating anonymous classes that can be assigned to constants or used directly.
The Ruby interpreter treats dynamically created classes identically to statically defined classes. Once created, these classes support inheritance, method definition, instance creation, and all standard class behaviors. The key difference lies in the timing and context of class definition.
# Static class definition
class Person
def initialize(name)
@name = name
end
end
# Dynamic class creation
Person = Class.new do
def initialize(name)
@name = name
end
end
Ruby's class creation system centers on the Class
class itself, which provides constructor methods and class modification capabilities. The Class.new
method accepts an optional superclass parameter and a block containing the class body. When no superclass is specified, the new class inherits from Object
.
The interpreter evaluates the block passed to Class.new
in the context of the new class, making self
refer to the class being defined. This context allows direct method definition using def
keywords and access to class-level methods like attr_accessor
, include
, and extend
.
# Creating a class with inheritance and methods
Animal = Class.new do
attr_reader :species
def initialize(species)
@species = species
end
def speak
"Generic animal sound"
end
end
Dog = Class.new(Animal) do
def speak
"Woof!"
end
end
dog = Dog.new("Canine")
puts dog.speak # => "Woof!"
puts dog.species # => "Canine"
Dynamic class creation becomes particularly useful in scenarios requiring conditional class definitions, factory patterns, or when class structure depends on runtime data. Configuration systems, ORMs, and DSL implementations frequently employ dynamic class generation to create specialized classes based on external specifications.
Basic Usage
The Class.new
constructor represents the fundamental approach to dynamic class creation. This method creates an anonymous class that can be assigned to a constant, stored in variables, or used directly without assignment.
# Basic class creation and assignment
Calculator = Class.new do
def add(a, b)
a + b
end
def multiply(a, b)
a * b
end
end
calc = Calculator.new
puts calc.add(5, 3) # => 8
puts calc.multiply(4, 7) # => 28
Anonymous classes created without constant assignment remain fully functional but lack a permanent name reference. The interpreter assigns temporary names like #<Class:0x00007f8b1c0a5d40>
to these classes, which can be retrieved using the name
method.
# Anonymous class without constant assignment
anonymous_class = Class.new do
def greet
"Hello from anonymous class!"
end
end
puts anonymous_class.name # => nil
instance = anonymous_class.new
puts instance.greet # => "Hello from anonymous class!"
The superclass parameter in Class.new
establishes inheritance relationships identical to static class definitions. The newly created class inherits all methods, constants, and behavior from its superclass.
# Class creation with inheritance
Vehicle = Class.new do
def initialize(wheels)
@wheels = wheels
end
def move
"Moving with #{@wheels} wheels"
end
end
Bicycle = Class.new(Vehicle) do
def initialize
super(2)
end
def pedal
"Pedaling the bicycle"
end
end
bike = Bicycle.new
puts bike.move # => "Moving with 2 wheels"
puts bike.pedal # => "Pedaling the bicycle"
Method definition within the Class.new
block follows standard Ruby syntax. The block executes in the context of the new class, allowing direct method definition, attribute declarations, and module inclusion.
# Complex class with multiple features
Product = Class.new do
include Comparable
attr_accessor :name, :price
def initialize(name, price)
@name = name
@price = price
end
def <=>(other)
price <=> other.price
end
def to_s
"#{name}: $#{price}"
end
# Class method definition
def self.create_bundle(products)
total_price = products.sum(&:price)
new("Bundle", total_price)
end
end
laptop = Product.new("Laptop", 999.99)
mouse = Product.new("Mouse", 29.99)
bundle = Product.create_bundle([laptop, mouse])
puts bundle # => "Bundle: $1029.98"
The define_method
method provides an alternative approach for adding methods to classes after creation. This method accepts a method name and block, creating instance methods dynamically.
# Adding methods after class creation
Reporter = Class.new
Reporter.define_method(:generate_report) do |data|
"Report generated with #{data.length} items"
end
Reporter.define_method(:format_data) do |items|
items.map(&:to_s).join(", ")
end
reporter = Reporter.new
data = [1, 2, 3, 4, 5]
puts reporter.generate_report(data) # => "Report generated with 5 items"
puts reporter.format_data(data) # => "1, 2, 3, 4, 5"
Advanced Usage
Dynamic class creation supports sophisticated patterns including factory methods, class hierarchies built from configuration data, and runtime class modification. These patterns enable flexible architectures that adapt to changing requirements or external specifications.
Factory methods can generate classes with varying behaviors based on parameters or configuration data. This approach proves particularly useful when class structure depends on runtime conditions or external data sources.
# Dynamic class factory with configuration
def create_api_client(service_name, endpoints)
Class.new do
define_singleton_method :service_name do
service_name
end
endpoints.each do |endpoint_name, config|
define_method(endpoint_name) do |params = {}|
# Simulate API call
{
service: self.class.service_name,
endpoint: endpoint_name,
method: config[:method],
params: params,
url: config[:url]
}
end
end
def initialize(api_key)
@api_key = api_key
end
private
attr_reader :api_key
end
end
# Configuration-driven class creation
payment_endpoints = {
create_charge: { method: 'POST', url: '/charges' },
get_charge: { method: 'GET', url: '/charges/:id' },
refund_charge: { method: 'POST', url: '/charges/:id/refund' }
}
PaymentAPI = create_api_client('PaymentService', payment_endpoints)
client = PaymentAPI.new('secret_key_123')
result = client.create_charge(amount: 1000, currency: 'USD')
puts result[:endpoint] # => :create_charge
puts result[:method] # => "POST"
Class hierarchies can be constructed dynamically by creating multiple related classes with shared behavior and specialized implementations. This pattern works well for plugin systems or when class structure maps to external data relationships.
# Dynamic class hierarchy generation
module DatabaseConnector
def self.create_model_class(table_name, columns, relationships = {})
model_class = Class.new do
define_singleton_method :table_name do
table_name.to_s
end
define_singleton_method :columns do
columns
end
# Generate attribute accessors
columns.each do |column_name, column_type|
attr_accessor column_name
# Type-specific validation methods
define_method("#{column_name}_valid?") do
value = instance_variable_get("@#{column_name}")
case column_type
when :string
value.is_a?(String)
when :integer
value.is_a?(Integer)
when :date
value.respond_to?(:strftime)
else
true
end
end
end
# Generate relationship methods
relationships.each do |rel_name, rel_config|
case rel_config[:type]
when :belongs_to
define_method(rel_name) do
rel_config[:class].find(instance_variable_get("@#{rel_config[:foreign_key]}"))
end
when :has_many
define_method(rel_name) do
rel_config[:class].where(rel_config[:foreign_key] => id)
end
end
end
def initialize(attributes = {})
attributes.each do |key, value|
instance_variable_set("@#{key}", value) if respond_to?("#{key}=")
end
end
def valid?
self.class.columns.all? { |column_name, _| send("#{column_name}_valid?") }
end
end
model_class
end
end
# Create models from schema definitions
User = DatabaseConnector.create_model_class(
:users,
{ id: :integer, name: :string, email: :string, created_at: :date }
)
Post = DatabaseConnector.create_model_class(
:posts,
{ id: :integer, title: :string, content: :string, user_id: :integer },
{ user: { type: :belongs_to, class: User, foreign_key: :user_id } }
)
user = User.new(id: 1, name: "John Doe", email: "john@example.com")
puts user.name_valid? # => true
puts user.valid? # => true
Metaprogramming techniques can extend dynamically created classes with additional capabilities through method generation, delegation patterns, and behavior modification. These approaches enable sophisticated abstractions and code reuse patterns.
# Advanced metaprogramming with dynamic classes
module Cacheable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def cacheable_method(method_name, cache_key_proc = nil)
original_method = instance_method(method_name)
define_method(method_name) do |*args|
cache_key = cache_key_proc ? cache_key_proc.call(self, *args) : "#{method_name}_#{args.hash}"
@method_cache ||= {}
if @method_cache.key?(cache_key)
@method_cache[cache_key]
else
result = original_method.bind(self).call(*args)
@method_cache[cache_key] = result
result
end
end
end
end
end
# Dynamic class with caching capabilities
def create_data_processor(operations)
Class.new do
include Cacheable
operations.each do |op_name, op_config|
define_method(op_name) do |data|
case op_config[:type]
when :transform
data.map(&op_config[:block])
when :filter
data.select(&op_config[:block])
when :reduce
data.reduce(&op_config[:block])
end
end
# Make method cacheable with custom cache key
cacheable_method(op_name, ->(instance, data) { "#{op_name}_#{data.hash}" })
end
def initialize(name)
@name = name
end
attr_reader :name
end
end
operations = {
double_values: { type: :transform, block: ->(x) { x * 2 } },
even_numbers: { type: :filter, block: ->(x) { x.even? } },
sum_all: { type: :reduce, block: ->(sum, x) { sum + x } }
}
Processor = create_data_processor(operations)
processor = Processor.new("NumberProcessor")
data = [1, 2, 3, 4, 5]
puts processor.double_values(data) # => [2, 4, 6, 8, 10] (cached)
puts processor.even_numbers(data) # => [2, 4] (cached)
puts processor.sum_all(data) # => 15 (cached)
Class modification after creation enables runtime behavior changes through method addition, removal, and redefinition. These capabilities support plugin architectures and adaptive system behavior.
# Runtime class modification and extension
ConfigurableService = Class.new do
def initialize
@plugins = []
end
def add_plugin(plugin_module)
@plugins << plugin_module
extend(plugin_module)
end
def available_methods
(methods - Object.methods).sort
end
attr_reader :plugins
end
# Plugin modules for runtime extension
module LoggingPlugin
def log(message)
puts "[#{Time.now}] #{message}"
end
def log_error(error)
puts "[#{Time.now}] ERROR: #{error}"
end
end
module ValidationPlugin
def validate_email(email)
email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
def validate_phone(phone)
phone.match?(/\A\d{3}-\d{3}-\d{4}\z/)
end
end
service = ConfigurableService.new
puts service.available_methods # => [:add_plugin, :available_methods, :plugins]
service.add_plugin(LoggingPlugin)
service.add_plugin(ValidationPlugin)
service.log("Service started") # => [timestamp] Service started
puts service.validate_email("test@example.com") # => true
puts service.available_methods.include?(:log) # => true
Common Pitfalls
Dynamic class creation introduces several subtle behaviors that can cause unexpected issues. Understanding these pitfalls helps avoid common mistakes and debugging difficulties.
Constant assignment timing affects class naming and reference behavior. Classes created with Class.new
remain anonymous until assigned to a constant, and the assignment location determines the constant's visibility scope.
# Pitfall: Late constant assignment affects class name
def create_user_class
Class.new do
def initialize(name)
@name = name
end
def to_s
"User: #{@name} (#{self.class.name})"
end
end
end
# Anonymous class - no constant assigned yet
user_class = create_user_class
user = user_class.new("John")
puts user.to_s # => "User: John ()"
# Constant assignment after creation
User = user_class
puts user.to_s # => "User: John (User)"
# Problem: Different assignment location creates different constant scope
class Namespace
# This creates Namespace::User, not ::User
LocalUser = create_user_class
end
local_user = Namespace::LocalUser.new("Jane")
puts local_user.class.name # => "Namespace::LocalUser"
Block scope and variable capture behavior can create unexpected closures and memory retention issues. Variables from the surrounding scope remain accessible within the class definition block, potentially causing memory leaks.
# Pitfall: Variable capture in class blocks
def create_processor_class(config_data)
# Large data structure captured in closure
expensive_data = Array.new(100_000) { |i| "Item #{i}" }
Class.new do
# This block captures expensive_data even if not used
define_method(:process) do |input|
# Only using config_data, but expensive_data is still captured
"Processing #{input} with config: #{config_data[:type]}"
end
# Attempting to access captured variable
define_method(:get_data_size) do
expensive_data.size # This works but keeps entire array in memory
end
end
end
config = { type: 'standard' }
ProcessorClass = create_processor_class(config)
processor = ProcessorClass.new
# Memory issue: expensive_data array remains in memory
# even though only config is actually needed
puts processor.process("input") # => "Processing input with config: standard"
puts processor.get_data_size # => 100000
# Solution: Pass only needed data to avoid capture
def create_processor_class_fixed(config_data)
config_type = config_data[:type] # Extract only needed value
Class.new do
define_method(:process) do |input|
"Processing #{input} with config: #{config_type}"
end
end
end
Method definition timing and context can cause confusion when methods are defined outside the class block or when using define_method
with incorrect binding.
# Pitfall: Method definition context confusion
class DynamicBuilder
def self.create_model(attributes)
model_class = Class.new
# Problem: Defining methods on the class from outside context
attributes.each do |attr_name|
# This works but is less clear than block definition
model_class.define_method(attr_name) do
instance_variable_get("@#{attr_name}")
end
model_class.define_method("#{attr_name}=") do |value|
instance_variable_set("@#{attr_name}", value)
end
end
# Problem: Adding instance method that references class-level data
creation_time = Time.now
model_class.define_method(:created_at) do
# This captures creation_time in closure - might not be intended
creation_time
end
model_class
end
# Better approach: Define methods in class block
def self.create_model_improved(attributes)
creation_time = Time.now
Class.new do
# Instance variable to store creation time per instance
def initialize
@creation_time = Time.now
end
attr_reader :creation_time
# Define attribute methods in proper context
attributes.each do |attr_name|
attr_accessor attr_name
end
end
end
end
# Problematic behavior with shared closure
Model1 = DynamicBuilder.create_model([:name, :age])
Model2 = DynamicBuilder.create_model([:title, :content])
instance1 = Model1.new
instance2 = Model2.new
# Both instances share the same creation_time from closure
puts instance1.created_at == instance2.created_at # => true (unexpected!)
Class hierarchy and inheritance issues arise when dynamically creating classes with complex inheritance chains or when superclass references become invalid.
# Pitfall: Inheritance and superclass reference issues
def create_base_class
Class.new do
def base_method
"Base implementation"
end
end
end
def create_child_class(parent_class)
Class.new(parent_class) do
def child_method
"Child implementation: #{super rescue 'no super'}"
end
def base_method
"Overridden: #{super}"
end
end
end
# Problem: Parent class reference can become invalid
BaseClass = create_base_class
ChildClass = create_child_class(BaseClass)
child = ChildClass.new
puts child.base_method # => "Overridden: Base implementation"
# Reassigning parent class constant doesn't affect inheritance
BaseClass = Class.new # Different class with same constant name
new_child = ChildClass.new
# Child still inherits from original BaseClass, not the new one
puts new_child.base_method # => "Overridden: Base implementation"
# Problem: Using variables instead of constants for superclass
def problematic_inheritance
parent_var = create_base_class
child_var = Class.new(parent_var) do
def test_method
"Child method"
end
end
# parent_var goes out of scope, but inheritance relationship persists
child_var
end
ProblematicChild = problematic_inheritance
instance = ProblematicChild.new
puts instance.class.superclass # => #<Class:0x...> (anonymous superclass)
Method visibility and access control require careful attention when using dynamic method definition, as the default visibility rules may not apply as expected.
# Pitfall: Method visibility in dynamic classes
class AccessControlDemo
def self.create_service_class
Class.new do
def public_method
"Public method can call: #{private_method}"
end
# Problem: Methods defined with define_method are public by default
define_method(:should_be_private) do
"This should be private but isn't!"
end
private
def private_method
"Private implementation"
end
# Methods defined after 'private' are private
define_method(:actually_private) do
"This is properly private"
end
end
end
end
ServiceClass = AccessControlDemo.create_service_class
service = ServiceClass.new
puts service.public_method # => "Public method can call: Private implementation"
# Problem: Method that should be private is actually public
puts service.should_be_private # => "This should be private but isn't!"
# This correctly raises an error
begin
puts service.actually_private
rescue NoMethodError => e
puts "Error: #{e.message}" # => Error: private method `actually_private' called
end
# Solution: Explicitly set visibility after definition
def create_service_class_fixed
Class.new do
def public_method
"Public: #{private_helper}"
end
define_method(:private_helper) do
"Helper method"
end
# Explicitly make the dynamically defined method private
private :private_helper
end
end
FixedService = create_service_class_fixed
fixed = FixedService.new
puts fixed.public_method # => "Public: Helper method"
# Now properly raises error
begin
puts fixed.private_helper
rescue NoMethodError => e
puts "Fixed: #{e.message}" # => Fixed: private method `private_helper' called
end
Reference
Core Class Creation Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Class.new(superclass = Object, &block) |
superclass (Class), block (Proc) | Class |
Creates new class with optional superclass and definition block |
#define_method(name, method = nil, &block) |
name (Symbol/String), method (Method/Proc), block (Proc) | Symbol |
Defines instance method with given name and implementation |
#define_singleton_method(name, &block) |
name (Symbol/String), block (Proc) | Symbol |
Defines singleton method on the class |
#remove_method(name) |
name (Symbol/String) | self |
Removes method definition from class |
#undef_method(name) |
name (Symbol/String) | self |
Undefines method, preventing inheritance |
Class Information Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#name |
none | String or nil |
Returns class name or nil for anonymous classes |
#superclass |
none | Class |
Returns direct superclass of the class |
#ancestors |
none | Array<Module> |
Returns array of class and included modules in lookup order |
#instance_methods(include_super = true) |
include_super (Boolean) | Array<Symbol> |
Returns array of instance method names |
#method_defined?(method_name) |
method_name (Symbol/String) | Boolean |
Checks if instance method is defined |
Method Visibility Control
Method | Parameters | Returns | Description |
---|---|---|---|
#private(*method_names) |
method_names (Symbol/String) | self |
Makes methods private |
#protected(*method_names) |
method_names (Symbol/String) | self |
Makes methods protected |
#public(*method_names) |
method_names (Symbol/String) | self |
Makes methods public |
#private_method_defined?(method_name) |
method_name (Symbol/String) | Boolean |
Checks if private method exists |
#protected_method_defined?(method_name) |
method_name (Symbol/String) | Boolean |
Checks if protected method exists |
Class Modification Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#include(*modules) |
modules (Module) | self |
Includes modules as mixins |
#extend(*modules) |
modules (Module) | self |
Extends class with module methods |
#prepend(*modules) |
modules (Module) | self |
Prepends modules to method lookup chain |
#alias_method(new_name, old_name) |
new_name (Symbol), old_name (Symbol) | self |
Creates method alias |
#module_eval(string = nil, &block) |
string (String), block (Proc) | Object |
Evaluates code in class context |
Common Class Creation Patterns
Pattern | Usage | Example |
---|---|---|
Anonymous Class | Temporary class without constant | Class.new { def test; end } |
Factory Method | Method returning configured class | def create_model(attrs); Class.new; end |
Inherited Class | Dynamic class with superclass | Class.new(BaseClass) { } |
Configuration Class | Class built from data | build_class_from_config(config) |
Plugin Class | Runtime-extensible class | class.extend(PluginModule) |
Error Types and Handling
Error Type | When Raised | Prevention |
---|---|---|
ArgumentError |
Invalid parameters to Class.new | Validate superclass and block parameters |
NameError |
Undefined method/constant reference | Check method existence before calling |
NoMethodError |
Calling undefined methods | Use respond_to? or method_defined? |
TypeError |
Wrong argument types | Validate argument types in factory methods |
SystemStackError |
Infinite recursion in inheritance | Avoid circular inheritance chains |
Performance Considerations
Scenario | Performance Impact | Optimization Strategy |
---|---|---|
Large closure capture | High memory usage | Extract only needed variables |
Frequent class creation | CPU overhead | Cache created classes when possible |
Deep inheritance chains | Method lookup cost | Limit inheritance depth |
Many dynamic methods | Memory per method | Use method_missing for similar methods |
Complex class hierarchies | Initialization cost | Lazy load class definitions |
Memory Management Guidelines
Pattern | Memory Impact | Best Practice |
---|---|---|
Variable capture in blocks | Retains closure scope | Minimize captured variables |
Anonymous class storage | No garbage collection | Assign to constants when persistent |
Method definition timing | Affects closure lifetime | Define methods in class block |
Large data in class scope | Shared across instances | Use instance variables instead |
Circular references | Prevents garbage collection | Use weak references when appropriate |
Class Naming Conventions
Naming Pattern | Visibility | Usage |
---|---|---|
CONSTANT = Class.new |
Global scope | Top-level class definition |
Module::CONSTANT = Class.new |
Module scope | Namespaced class |
variable = Class.new |
Local scope | Temporary or dynamic usage |
Anonymous | No constant reference | Single-use or factory patterns |
Debugging Dynamic Classes
Issue | Diagnostic Method | Solution |
---|---|---|
Anonymous class identification | class.name inspection |
Assign to constant |
Method lookup problems | ancestors examination |
Check inheritance chain |
Variable capture issues | Memory profiling | Minimize closure variables |
Visibility problems | private_methods listing |
Explicit visibility control |
Performance bottlenecks | Benchmarking | Profile class creation vs usage |