Overview
Dynamic module creation in Ruby enables developers to construct and modify modules programmatically during runtime. Ruby provides several mechanisms for this functionality, including Module.new
, define_method
, const_set
, and various reflection methods. This approach differs from static module definition by allowing the structure, methods, and behavior of modules to be determined by runtime conditions, configuration data, or external inputs.
The primary classes involved in dynamic module creation are Module
and Class
, along with supporting methods from Object
and Kernel
. The Module
class serves as the foundation, providing methods for creating anonymous modules, defining methods dynamically, and managing constants. The Class
class inherits from Module
and adds constructor behavior, making it suitable for creating both modules and classes programmatically.
Ruby's module system operates through a method lookup chain that includes modules mixed into classes via include
, prepend
, and extend
. Dynamic module creation leverages this system by constructing modules that integrate seamlessly with existing code. The interpreter handles method resolution, constant lookup, and inheritance relationships automatically, regardless of whether modules are defined statically or created dynamically.
# Basic anonymous module creation
dynamic_module = Module.new do
def greet(name)
"Hello, #{name}!"
end
end
class User
include dynamic_module
end
user = User.new
user.greet("Alice") # => "Hello, Alice!"
Common use cases include plugin architectures, configuration-driven behavior, code generation from external schemas, and runtime adaptation to changing requirements. Dynamic modules excel in scenarios where the exact structure of code cannot be predetermined, such as API clients generated from service descriptions, domain-specific languages, or frameworks that need to adapt to user-defined schemas.
# Configuration-driven module creation
def create_validator_module(rules)
Module.new do
rules.each do |field, validations|
define_method("validate_#{field}") do |value|
validations.all? { |validation| validation.call(value) }
end
end
end
end
validation_rules = {
email: [proc { |v| v.include?("@") }, proc { |v| v.length > 5 }],
age: [proc { |v| v.is_a?(Integer) }, proc { |v| v > 0 }]
}
validator = create_validator_module(validation_rules)
The module creation process involves several steps: instantiating the module object, defining its interface through methods and constants, and integrating it with existing classes or other modules. Ruby handles memory management and garbage collection for dynamically created modules, cleaning up unused modules when they become unreachable.
Basic Usage
The Module.new
constructor creates anonymous modules that can be assigned to constants or used directly. When called with a block, the block executes in the context of the new module, allowing method definitions and constant assignments within the module's scope. This pattern forms the foundation of most dynamic module creation scenarios.
# Anonymous module with block initialization
StringUtils = Module.new do
def self.reverse_words(text)
text.split.reverse.join(" ")
end
def titleize
self.split.map(&:capitalize).join(" ")
end
end
String.include(StringUtils)
"hello world".titleize # => "Hello World"
StringUtils.reverse_words("hello world") # => "world hello"
The define_method
method creates instance methods dynamically within modules. Unlike def
statements, define_method
accepts the method name as a parameter, enabling method creation based on runtime data. The method body can be provided as a block or a Proc
object, and the method gains access to variables from the surrounding lexical scope through closure behavior.
# Dynamic method definition from configuration
def create_accessor_module(attributes)
Module.new do
attributes.each do |attr_name, default_value|
define_method(attr_name) do
instance_variable_get("@#{attr_name}") || default_value
end
define_method("#{attr_name}=") do |value|
instance_variable_set("@#{attr_name}", value)
end
end
end
end
UserAttributes = create_accessor_module({
name: "Anonymous",
email: nil,
active: true
})
class User
include UserAttributes
end
user = User.new
user.name # => "Anonymous"
user.email = "test@example.com"
user.email # => "test@example.com"
Constants within dynamic modules are managed using const_set
and const_get
methods. These methods operate on the module's constant table, allowing runtime assignment and retrieval of constant values. Constants defined this way follow the same scoping and lookup rules as statically defined constants, including inheritance and nested constant resolution.
# Dynamic constant definition
APIModule = Module.new do
const_set(:VERSION, "2.1.0")
const_set(:BASE_URL, "https://api.example.com")
endpoints = ["users", "posts", "comments"]
endpoints.each do |endpoint|
const_set(endpoint.upcase, "/#{endpoint}")
end
end
APIModule::VERSION # => "2.1.0"
APIModule::USERS # => "/users"
APIModule::POSTS # => "/posts"
Module inclusion and extension work identically with dynamic modules as with static modules. The include
method adds the module's instance methods to the including class, while extend
adds them as singleton methods. The prepend
method, introduced in Ruby 2.0, adds the module's methods earlier in the method lookup chain than the class's own methods.
# Dynamic module with different inclusion strategies
LoggingModule = Module.new do
def log_action(action)
puts "[#{Time.now}] #{self.class.name}: #{action}"
end
def self.included(base)
puts "LoggingModule included in #{base.name}"
end
def self.extended(base)
puts "LoggingModule extended by #{base.name}"
end
end
class Service
include LoggingModule
end
class Utility
extend LoggingModule
end
Service.new.log_action("processing") # Instance method
Utility.log_action("calculating") # Class method
Advanced Usage
Module builders represent a sophisticated pattern for creating families of related modules with shared structure but varying behavior. These builders encapsulate the module creation logic within classes or modules, providing a clean interface for generating modules based on complex specifications or configurations.
class ServiceModuleBuilder
def initialize(service_name, base_url)
@service_name = service_name
@base_url = base_url
@endpoints = {}
@middleware = []
end
def add_endpoint(name, path, http_method = :get)
@endpoints[name] = { path: path, method: http_method }
self
end
def add_middleware(middleware_proc)
@middleware << middleware_proc
self
end
def build
service_name = @service_name
base_url = @base_url
endpoints = @endpoints
middleware = @middleware
Module.new do
const_set(:SERVICE_NAME, service_name)
const_set(:BASE_URL, base_url)
endpoints.each do |name, config|
define_method(name) do |params = {}|
url = "#{base_url}#{config[:path]}"
request_data = { method: config[:method], url: url, params: params }
# Apply middleware transformations
middleware.each do |mw|
request_data = mw.call(request_data)
end
# Simulated HTTP request
"#{config[:method].upcase} #{request_data[:url]} with #{request_data[:params]}"
end
end
define_method(:service_info) do
"Service: #{service_name}, Base URL: #{base_url}"
end
end
end
end
# Usage of the builder
api_client = ServiceModuleBuilder
.new("UserService", "https://api.users.com")
.add_endpoint(:get_user, "/users/:id")
.add_endpoint(:create_user, "/users", :post)
.add_middleware(proc { |req| req[:headers] = { "Accept" => "application/json" }; req })
.build
class UserManager
include api_client
end
manager = UserManager.new
manager.get_user(id: 123) # => "GET https://api.users.com/users/:id with {:id=>123}"
Nested module creation involves constructing modules within other modules, creating hierarchical namespaces dynamically. This pattern requires careful handling of constant scoping and method visibility, as nested modules inherit certain behaviors from their parent modules.
def create_nested_api_modules(service_configs)
root_module = Module.new
service_configs.each do |service_name, config|
service_module = Module.new do
const_set(:VERSION, config[:version])
config[:resources].each do |resource_name, methods|
resource_module = Module.new do
methods.each do |method_name, implementation|
define_method(method_name, &implementation)
end
end
const_set(resource_name.to_s.capitalize, resource_module)
end
end
root_module.const_set(service_name.to_s.capitalize, service_module)
end
root_module
end
config = {
billing: {
version: "1.0",
resources: {
invoices: {
create: proc { |data| "Creating invoice with #{data}" },
list: proc { "Listing all invoices" }
},
payments: {
process: proc { |amount| "Processing payment of #{amount}" }
}
}
},
inventory: {
version: "2.0",
resources: {
items: {
stock_check: proc { |id| "Checking stock for item #{id}" }
}
}
}
}
APIModules = create_nested_api_modules(config)
# Access nested functionality
billing_invoices = APIModules::Billing::Invoices
inventory_items = APIModules::Inventory::Items
class OrderProcessor
include billing_invoices
include inventory_items
end
processor = OrderProcessor.new
processor.create(data: { customer_id: 1, amount: 100 }) # => "Creating invoice with {...}"
processor.stock_check(42) # => "Checking stock for item 42"
Method delegation and forwarding within dynamic modules enables the creation of proxy objects and adapter patterns. These patterns involve creating methods that forward calls to other objects or modules, often with transformation or validation logic applied during the forwarding process.
def create_delegating_module(target_methods)
Module.new do
target_methods.each do |local_name, target_config|
target_object = target_config[:to]
target_method = target_config[:method] || local_name
transform_proc = target_config[:transform]
define_method(local_name) do |*args, **kwargs|
result = target_object.public_send(target_method, *args, **kwargs)
transform_proc ? transform_proc.call(result) : result
end
end
end
end
# External service objects
email_service = Object.new
def email_service.send_email(to, subject, body)
"Email sent to #{to}: #{subject}"
end
sms_service = Object.new
def sms_service.send_sms(phone, message)
"SMS sent to #{phone}: #{message}"
end
# Create delegating notification module
NotificationModule = create_delegating_module({
send_email: {
to: email_service,
method: :send_email
},
send_notification: {
to: sms_service,
method: :send_sms,
transform: proc { |result| "[NOTIFICATION] #{result}" }
}
})
class NotificationCenter
include NotificationModule
end
center = NotificationCenter.new
center.send_email("user@example.com", "Welcome", "Hello!")
# => "Email sent to user@example.com: Welcome"
center.send_notification("555-0123", "Alert!")
# => "[NOTIFICATION] SMS sent to 555-0123: Alert!"
Common Pitfalls
Constant assignment timing creates subtle bugs when constants are set within dynamic modules after the module has been included in classes. Ruby resolves constants at the time of first access, not at include time, which can lead to NameError
exceptions if constants are assigned after inclusion but before first use.
# Problematic constant assignment timing
ProblematicModule = Module.new
class User
include ProblematicModule # Module included before constants are set
end
# Constants assigned after inclusion
ProblematicModule.const_set(:DEFAULT_ROLE, "guest")
ProblematicModule.const_set(:VALID_STATUSES, ["active", "inactive"])
# This works fine
User.new.class::DEFAULT_ROLE # => "guest"
# But this fails in method definitions within the module
ProblematicModule.module_eval do
def get_default_role
DEFAULT_ROLE # NameError: uninitialized constant unless carefully handled
end
end
# Correct approach: set constants before inclusion
CorrectModule = Module.new do
const_set(:DEFAULT_ROLE, "guest")
const_set(:VALID_STATUSES, ["active", "inactive"])
def get_default_role
DEFAULT_ROLE # Works correctly
end
end
Scope capture in closures can cause memory leaks and unexpected behavior when large objects are inadvertently captured by method definitions within dynamic modules. The closure created by blocks passed to define_method
retains references to all variables in scope, potentially preventing garbage collection of large data structures.
# Memory leak through closure capture
def create_leaky_module(large_dataset)
processed_data = large_dataset.map { |item| expensive_processing(item) }
summary = generate_summary(processed_data)
Module.new do
# This closure captures both large_dataset AND processed_data
define_method(:get_summary) do
summary # Only needs summary, but closure captures everything
end
# Better approach: extract needed values first
summary_value = summary
define_method(:get_summary_safe) do
summary_value # Only captures the specific value needed
end
end
end
# Even better: use module_eval with string to avoid closures entirely
def create_safe_module(large_dataset)
summary = generate_summary(large_dataset)
Module.new.tap do |mod|
mod.const_set(:SUMMARY, summary)
mod.module_eval <<-RUBY
def get_summary
SUMMARY
end
RUBY
end
end
Method visibility inheritance behaves unexpectedly with dynamic modules when methods are defined as private or protected. The visibility setting applies to methods defined after the visibility declaration, but dynamic method definition timing can create inconsistent visibility across methods within the same module.
# Visibility confusion with dynamic methods
VisibilityModule = Module.new do
def public_method
"This is public"
end
private # This affects subsequent method definitions
def private_static_method
"This is private"
end
# Methods defined dynamically after 'private' are still private
define_method(:private_dynamic_method) do
"This is also private"
end
public # Reset visibility
define_method(:public_dynamic_method) do
"This is public again"
end
end
class TestClass
include VisibilityModule
end
test = TestClass.new
test.public_method # Works
test.public_dynamic_method # Works
# test.private_static_method # NoMethodError: private method
# test.private_dynamic_method # NoMethodError: private method
# Explicit visibility control for dynamic methods
ExplicitVisibilityModule = Module.new do
define_method(:helper_method) { "Helper logic" }
private :helper_method # Explicitly make private after definition
define_method(:public_interface) do
"Public: #{helper_method}" # Can call private method internally
end
end
Module evaluation context confusion arises when module_eval
, class_eval
, and instance_eval
are used interchangeably without understanding their different execution contexts. Each method evaluates code in a different binding, affecting variable access, method definitions, and constant lookup.
# Context confusion demonstration
outer_variable = "accessible"
TestModule = Module.new
# module_eval executes in the module's context
TestModule.module_eval do
puts outer_variable # Accessible through closure
def module_eval_method
"Defined as instance method"
end
define_singleton_method(:module_singleton) do
"Defined as module singleton method"
end
end
# instance_eval executes in the context of the module object itself
TestModule.instance_eval do
puts outer_variable # Still accessible through closure
def instance_eval_method
"Defined as singleton method on the module"
end
end
# The methods end up in different places:
class TestClass
include TestModule
end
TestClass.new.module_eval_method # Instance method from module_eval
TestModule.module_singleton # Module singleton method
TestModule.instance_eval_method # Module singleton method
# class_eval is an alias for module_eval but suggests class context
# Use module_eval for modules, class_eval for classes for clarity
Include order dependencies become critical when multiple dynamic modules define methods with the same name or when modules depend on methods defined in other modules. Ruby's method lookup chain follows the reverse order of inclusion, and dynamic module creation can make these dependencies non-obvious.
# Include order dependency issues
BaseModule = Module.new do
def process_data(data)
"Base processing: #{data}"
end
end
# This module depends on process_data being available
EnhancingModule = Module.new do
def enhanced_process(data)
"Enhanced: #{process_data(data)}" # Expects process_data to exist
end
end
# Wrong order - EnhancingModule included before BaseModule
class ProblematicProcessor
include EnhancingModule # This module needs process_data
include BaseModule # This module provides process_data
end
# This works because method lookup finds process_data in BaseModule
processor = ProblematicProcessor.new
processor.enhanced_process("test") # => "Enhanced: Base processing: test"
# But consider this case where dependency is not met:
StandaloneEnhancing = Module.new do
def standalone_enhanced(data)
"Standalone: #{process_data(data)}" # Will fail if no process_data
end
end
class FailingProcessor
include StandaloneEnhancing # No process_data available
end
# This fails at runtime
# FailingProcessor.new.standalone_enhanced("test") # NoMethodError
Reference
Module Creation Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Module.new |
&block (optional) |
Module |
Creates anonymous module, evaluates block in module context if provided |
Module.new { ... } |
Block | Module |
Creates module and executes block in module's context |
Class.new |
superclass = Object , &block |
Class |
Creates anonymous class with optional superclass and initialization block |
Class.new(SuperClass) { ... } |
Superclass, block | Class |
Creates class inheriting from superclass with block evaluation |
Dynamic Method Definition
Method | Parameters | Returns | Description |
---|---|---|---|
define_method(name) |
Symbol/String, block | Symbol | Defines instance method with given name and block body |
define_method(name, method) |
Symbol/String, Method/Proc | Symbol | Defines instance method using existing method or proc |
define_singleton_method(name) |
Symbol/String, block | Symbol | Defines singleton method on the module/class object |
alias_method(new_name, old_name) |
Symbol/String, Symbol/String | Module | Creates alias for existing method |
undef_method(name) |
Symbol/String | Module | Removes method definition from module |
remove_method(name) |
Symbol/String | Module | Removes method from current class/module only |
Constant Management
Method | Parameters | Returns | Description |
---|---|---|---|
const_set(name, value) |
Symbol/String, Object | Object | Assigns constant within module namespace |
const_get(name) |
Symbol/String, Boolean (inherit) | Object | Retrieves constant value, optionally searching ancestors |
const_defined?(name) |
Symbol/String, Boolean (inherit) | Boolean | Checks if constant is defined in module or ancestors |
constants |
Boolean (inherit) | Array | Returns array of constant names defined in module |
const_missing(name) |
Symbol | Object | Hook called when constant lookup fails, can be overridden |
Module Inclusion and Extension
Method | Parameters | Returns | Description |
---|---|---|---|
include(*modules) |
Module(s) | self | Adds modules as ancestors, methods become instance methods |
prepend(*modules) |
Module(s) | self | Adds modules early in ancestor chain, before class methods |
extend(*modules) |
Module(s) | self | Adds module methods as singleton methods on receiver |
included_modules |
None | Array | Returns modules included in receiver's ancestor chain |
ancestors |
None | Array | Returns complete ancestor chain including classes and modules |
Code Evaluation Methods
Method | Parameters | Returns | Description |
---|---|---|---|
module_eval(string) |
String | Object | Evaluates string in module context |
module_eval { block } |
Block | Object | Evaluates block in module context |
class_eval(string) |
String | Object | Alias for module_eval, preferred for classes |
instance_eval(string) |
String | Object | Evaluates string in context of module object |
instance_eval { block } |
Block | Object | Evaluates block in context of module object |
Callback Hooks
Hook Method | When Called | Parameters | Description |
---|---|---|---|
included(base) |
When module is included | Including class/module | Called after module inclusion |
extended(base) |
When module is extended | Extending object | Called after module extension |
prepended(base) |
When module is prepended | Prepending class/module | Called after module prepending |
const_missing(name) |
When constant not found | Constant name | Opportunity to define missing constant |
method_missing(name, *args) |
When method not found | Method name, arguments | Handle undefined method calls |
Introspection and Reflection
Method | Parameters | Returns | Description |
---|---|---|---|
instance_methods |
Boolean (include_super) | Array | Returns instance method names |
private_instance_methods |
Boolean (include_super) | Array | Returns private instance method names |
singleton_methods |
Boolean (all) | Array | Returns singleton method names |
method_defined?(name) |
Symbol/String | Boolean | Checks if instance method is defined |
private_method_defined?(name) |
Symbol/String | Boolean | Checks if private method is defined |
respond_to?(name) |
Symbol/String, Boolean (include_private) | Boolean | Checks if object responds to method |
Visibility Control
Method | Parameters | Returns | Description |
---|---|---|---|
private(*names) |
Symbol/String(s) | Module | Makes methods private |
protected(*names) |
Symbol/String(s) | Module | Makes methods protected |
public(*names) |
Symbol/String(s) | Module | Makes methods public |
private_constant(*names) |
Symbol/String(s) | Module | Makes constants private |
public_constant(*names) |
Symbol/String(s) | Module | Makes constants public |
Common Error Classes
Error Class | Common Causes | Description |
---|---|---|
NameError |
Undefined constant, undefined method | Name resolution failures |
NoMethodError |
Method not found, private method called | Method call failures |
ArgumentError |
Wrong argument count, invalid arguments | Argument validation failures |
TypeError |
Wrong object type, conversion failures | Type-related errors |
RuntimeError |
General runtime issues | Default error for raise |