CrackedRuby logo

CrackedRuby

Dynamic Class Creation

Dynamic class creation in Ruby allows programs to define classes at runtime using metaprogramming techniques and the Class.new constructor.

Metaprogramming Code Generation
5.8.1

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