CrackedRuby logo

CrackedRuby

Module Structure

Ruby module organization, nesting, inclusion patterns, and namespace management for code reuse and encapsulation.

Patterns and Best Practices Code Organization
11.3.1

Overview

Ruby modules serve as containers for methods, constants, and other modules, providing namespacing and mixins for code organization. The module system operates through three primary mechanisms: definition with module, inclusion via include, extend, and prepend, and nesting for hierarchical organization.

Modules cannot be instantiated directly but integrate with classes through Ruby's method lookup chain. When included, a module's instance methods become available to the including class. Extension adds module methods as singleton methods to the target object. Prepending inserts the module before the class in the method lookup chain, enabling method interception patterns.

module Authenticatable
  def authenticate(password)
    encrypted_password == encrypt(password)
  end
  
  private
  
  def encrypt(value)
    # encryption logic
  end
end

class User
  include Authenticatable
  attr_accessor :encrypted_password
end

user = User.new
user.encrypted_password = "abc123"
user.authenticate("plaintext")
# => false (assuming encrypt returns different value)

Module nesting creates hierarchical namespaces that prevent naming conflicts and organize related functionality. Nested modules access outer scope through constant resolution rules, with Ruby searching from innermost to outermost scope.

module Payment
  PROCESSING_FEE = 2.5
  
  module CreditCard
    def self.process(amount)
      total = amount + PROCESSING_FEE  # accesses outer constant
      "Processing $#{total} via credit card"
    end
  end
  
  module BankTransfer
    PROCESSING_FEE = 1.0  # shadows outer constant
    
    def self.process(amount)
      total = amount + PROCESSING_FEE  # uses inner constant
      "Processing $#{total} via bank transfer"
    end
  end
end

Ruby provides callback methods (included, extended, prepended) that execute when modules integrate with classes, enabling dynamic behavior modification and hook registration.

Basic Usage

Module definition uses the module keyword followed by a constant name. Module names follow class naming conventions with CamelCase. Methods defined within modules become instance methods when included or singleton methods when extended.

module Comparable
  def >(other)
    (self <=> other) > 0
  end
  
  def <(other)
    (self <=> other) < 0
  end
  
  def ==(other)
    (self <=> other) == 0
  end
end

class Version
  include Comparable
  attr_reader :major, :minor, :patch
  
  def initialize(version_string)
    @major, @minor, @patch = version_string.split('.').map(&:to_i)
  end
  
  def <=>(other)
    [major, minor, patch] <=> [other.major, other.minor, other.patch]
  end
end

v1 = Version.new("2.1.0")
v2 = Version.new("2.0.5")
v1 > v2  # => true

The include method adds module methods as instance methods to the including class. Multiple modules can be included, with later inclusions appearing first in the method lookup chain. Module methods override class methods when names conflict.

module Debuggable
  def debug(message)
    puts "[DEBUG] #{self.class.name}: #{message}"
  end
end

module Timestamped
  def with_timestamp(message)
    "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} - #{message}"
  end
end

class APIRequest
  include Debuggable
  include Timestamped
  
  def process
    debug(with_timestamp("Processing request"))
  end
end

request = APIRequest.new
request.process
# [DEBUG] APIRequest: 2023-10-15 14:30:25 - Processing request

Module extension with extend adds module methods as singleton methods to the target object. This pattern works with classes, instances, and other modules. Extended methods become class methods when applied to classes.

module Configurable
  def configure(&block)
    instance_eval(&block) if block_given?
  end
  
  def config
    @config ||= {}
  end
  
  def set(key, value)
    config[key] = value
  end
  
  def get(key)
    config[key]
  end
end

class Database
  extend Configurable
end

Database.configure do
  set :host, 'localhost'
  set :port, 5432
  set :database, 'myapp'
end

Database.get(:host)  # => "localhost"

Nested modules create namespaces that group related functionality and prevent naming conflicts. Access nested modules using the scope resolution operator ::. Constants defined in outer modules remain accessible to inner modules unless shadowed by local definitions.

module GraphicsEngine
  RENDER_QUALITY = 'high'
  
  module Shapes
    class Circle
      def render
        "Rendering circle with #{RENDER_QUALITY} quality"
      end
    end
    
    class Rectangle
      def render
        "Rendering rectangle with #{RENDER_QUALITY} quality"
      end
    end
  end
  
  module Effects
    RENDER_QUALITY = 'ultra'  # shadows outer constant
    
    class Glow
      def render
        "Rendering glow effect with #{RENDER_QUALITY} quality"
      end
    end
  end
end

circle = GraphicsEngine::Shapes::Circle.new
circle.render  # => "Rendering circle with high quality"

glow = GraphicsEngine::Effects::Glow.new  
glow.render    # => "Rendering glow effect with ultra quality"

Advanced Usage

Module prepending with prepend inserts modules before the including class in the method lookup chain, enabling method interception and decoration patterns. Prepended modules can call super to invoke the original method implementation, creating wrapper behavior around existing functionality.

module Cached
  def find(id)
    cache_key = "#{self.name}:#{id}"
    
    if cached_value = cache_store[cache_key]
      puts "Cache hit for #{cache_key}"
      return cached_value
    end
    
    result = super  # calls original find method
    cache_store[cache_key] = result
    puts "Cached result for #{cache_key}"
    result
  end
  
  private
  
  def cache_store
    @cache_store ||= {}
  end
end

module Logged
  def find(id)
    start_time = Time.now
    puts "Starting find operation for ID: #{id}"
    
    result = super
    
    duration = Time.now - start_time
    puts "Find operation completed in #{duration.round(3)}s"
    result
  end
end

class User
  prepend Cached
  prepend Logged  # this executes first
  
  def self.find(id)
    sleep(0.1)  # simulate database query
    { id: id, name: "User #{id}", email: "user#{id}@example.com" }
  end
end

User.find(1)
# Starting find operation for ID: 1
# Cached result for 1
# Find operation completed in 0.101s

User.find(1)  
# Starting find operation for ID: 1
# Cache hit for User:1
# Find operation completed in 0.001s

Module callbacks execute when modules interact with classes, providing hooks for dynamic behavior setup. The included callback runs when a module is included, receiving the including class as an argument. This enables class method addition and configuration.

module Trackable
  def self.included(base)
    base.extend(ClassMethods)
    base.class_eval do
      attr_accessor :tracked_attributes
      after_initialize :setup_tracking
    end
  end
  
  module ClassMethods
    def track(*attributes)
      tracked_attributes.concat(attributes.map(&:to_s))
    end
    
    def tracked_attributes
      @tracked_attributes ||= []
    end
  end
  
  def setup_tracking
    @changes = {}
    @original_values = {}
    
    self.class.tracked_attributes.each do |attr|
      @original_values[attr] = send(attr)
      define_singleton_method("#{attr}=") do |value|
        old_value = send(attr)
        @changes[attr] = [old_value, value] if old_value != value
        instance_variable_set("@#{attr}", value)
      end
    end
  end
  
  def changes
    @changes.dup
  end
  
  def changed?
    @changes.any?
  end
end

class Product
  include Trackable
  attr_accessor :name, :price, :description
  
  track :name, :price
  
  def initialize(name, price)
    @name = name
    @price = price
    setup_tracking
  end
  
  private
  
  def after_initialize(method_name)
    # simplified callback simulation
  end
end

Module composition creates complex behaviors by combining multiple modules. The order of inclusion affects method resolution, with later modules taking precedence. This enables layered functionality and aspect-oriented programming patterns.

module Validatable
  def validate!
    errors = []
    self.class.validations.each do |field, rules|
      value = send(field)
      rules.each do |rule, parameter|
        case rule
        when :presence
          errors << "#{field} cannot be blank" if value.nil? || value == ""
        when :length
          errors << "#{field} too long" if value.to_s.length > parameter
        end
      end
    end
    raise StandardError, errors.join(", ") unless errors.empty?
  end
  
  def self.included(base)
    base.extend(ValidationMethods)
  end
  
  module ValidationMethods
    def validates(field, **rules)
      validations[field] = rules
    end
    
    def validations
      @validations ||= {}
    end
  end
end

module Serializable
  def to_json
    attributes = {}
    self.class.serialized_attributes.each do |attr|
      attributes[attr] = send(attr)
    end
    JSON.generate(attributes)
  end
  
  def self.included(base)
    base.extend(SerializationMethods)
  end
  
  module SerializationMethods
    def serialize(*attributes)
      serialized_attributes.concat(attributes.map(&:to_s))
    end
    
    def serialized_attributes
      @serialized_attributes ||= []
    end
  end
end

class Customer
  include Validatable
  include Serializable
  attr_accessor :name, :email, :age
  
  validates :name, presence: true, length: 50
  validates :email, presence: true
  serialize :name, :email, :age
  
  def initialize(name, email, age)
    @name = name
    @email = email  
    @age = age
  end
end

Dynamic module creation enables runtime module generation and modification. This pattern supports plugin architectures and configuration-driven behavior where module structure depends on external conditions.

def create_api_client_module(base_url, endpoints)
  Module.new do
    endpoints.each do |endpoint, config|
      define_method(endpoint) do |**params|
        url = "#{base_url}#{config[:path]}"
        method = config[:method] || :get
        
        case method
        when :get
          # simulate GET request
          "GET #{url} with params: #{params}"
        when :post
          # simulate POST request  
          "POST #{url} with data: #{params}"
        end
      end
    end
    
    define_method(:base_url) { base_url }
  end
end

api_config = {
  users: { path: '/users', method: :get },
  create_user: { path: '/users', method: :post },
  orders: { path: '/orders', method: :get }
}

APIClient = create_api_client_module('https://api.example.com', api_config)

class ServiceLayer
  include APIClient
end

service = ServiceLayer.new
service.users(page: 1, limit: 10)
# => "GET https://api.example.com/users with params: {:page=>1, :limit=>10}"

Common Pitfalls

Constant resolution in nested modules follows Ruby's lexical scoping rules, not inheritance chains. When a constant is referenced within a nested module, Ruby searches the nesting chain from innermost to outermost scope, then checks top-level constants. This behavior creates confusion when constants exist at multiple nesting levels.

TIMEOUT = 30  # top-level constant

module NetworkClient
  TIMEOUT = 10
  
  module HTTP
    def self.request(url)
      # This uses NetworkClient::TIMEOUT, not top-level TIMEOUT
      "Making HTTP request with #{TIMEOUT}s timeout"  # => 10s timeout
    end
    
    def self.fully_qualified_request(url)
      # Explicitly reference top-level constant
      "Making HTTP request with #{::TIMEOUT}s timeout"  # => 30s timeout
    end
  end
  
  module FTP
    TIMEOUT = 60  # shadows outer TIMEOUT
    
    def self.transfer(file)
      # This uses FTP::TIMEOUT (60), not NetworkClient::TIMEOUT (10)
      "Transferring file with #{TIMEOUT}s timeout"  # => 60s timeout
    end
  end
end

NetworkClient::HTTP.request("example.com")
# => "Making HTTP request with 10s timeout"

NetworkClient::FTP.transfer("file.txt") 
# => "Transferring file with 60s timeout"

Method visibility in modules propagates to including classes, but the visibility context depends on where methods are defined. Private and protected methods in modules remain private/protected when included. However, extending a module with private methods makes those methods private singleton methods, often causing unexpected behavior.

module DatabaseHelpers
  def connection
    @connection ||= establish_connection
  end
  
  private
  
  def establish_connection
    "Database connection established"
  end
  
  def log_query(sql)
    puts "Executing: #{sql}"
  end
end

class User
  include DatabaseHelpers
  
  def find_by_email(email)
    log_query("SELECT * FROM users WHERE email = '#{email}'")  # Error!
    # private method `log_query' called
  end
  
  def safe_find_by_email(email) 
    connection  # This works - connection is public
    send(:log_query, "SELECT * FROM users WHERE email = '#{email}'")
  end
end

# Extending creates different visibility behavior
class Report
  extend DatabaseHelpers
end

# Report.establish_connection  # Error! private method
# Report.send(:establish_connection)  # This works

Multiple inclusion of the same module does not execute inclusion callbacks multiple times, but module nesting and method lookup can create unexpected behavior when modules share names across different nesting levels. Ruby's module inclusion is idempotent - including the same module multiple times has no additional effect.

module Loggable
  def self.included(base)
    puts "Loggable included in #{base.name}"
  end
  
  def log(message)
    puts "[LOG] #{message}"
  end
end

class Service
  include Loggable  # "Loggable included in Service" 
  include Loggable  # No output - already included
end

# Name collision example
module Admin
  module User
    def role
      "administrator"
    end
  end
end

module Guest  
  module User
    def role
      "guest"
    end
  end
end

class Session
  include Admin::User
  include Guest::User  # This overrides Admin::User methods
end

session = Session.new
session.role  # => "guest" (not "administrator")

Module variables and state sharing can create memory leaks and unexpected behavior when modules maintain instance variables or class variables. Since modules can be included in multiple classes, shared state becomes problematic across different contexts.

module Cacheable
  def self.included(base)
    # This creates a class variable shared across ALL including classes
    base.class_variable_set(:@@cache, {})
  end
  
  def cached_get(key)
    cache = self.class.class_variable_get(:@@cache)
    cache[key]
  end
  
  def cached_set(key, value)
    cache = self.class.class_variable_get(:@@cache) 
    cache[key] = value
  end
end

class Product
  include Cacheable
end

class Order
  include Cacheable  
end

# Unexpected sharing - both classes share the same cache!
Product.new.cached_set("key1", "product_value")
Order.new.cached_get("key1")  # => "product_value" (unexpected!)

# Better approach - use instance variables per class
module BetterCacheable
  def self.included(base)
    base.instance_variable_set(:@cache, {})
    base.define_singleton_method(:cache) do
      @cache
    end
  end
  
  def cached_get(key)
    self.class.cache[key]
  end
  
  def cached_set(key, value)
    self.class.cache[key] = value
  end
end

Circular dependencies between modules can create loading issues and undefined constant errors, particularly in larger applications with complex module hierarchies. Ruby loads constants lazily, so forward references may fail depending on load order.

# In file user.rb
module UserHelpers
  def format_name
    # This may fail if AddressHelpers not loaded yet
    AddressHelpers.format_location(self.location)
  end
end

# In file address.rb  
module AddressHelpers
  def self.format_location(location)
    # This creates a circular dependency
    UserHelpers.some_method if defined?(UserHelpers)
    "Formatted: #{location}"
  end
end

# Better approach - avoid circular dependencies
module Formatters
  module User
    def self.format_name(user, location_formatter = AddressFormatters)
      location_formatter.format_location(user.location)
    end
  end
  
  module Address
    def self.format_location(location)
      "Formatted: #{location}"
    end
  end
end

Reference

Core Module Methods

Method Parameters Returns Description
module Name Name (Constant) Module Defines a new module with given name
#include(module) module (Module) self Includes module as instance methods
#extend(module) module (Module) self Extends object with module methods as singleton methods
#prepend(module) module (Module) self Prepends module before class in method lookup
#included_modules None Array Returns array of included modules
#ancestors None Array Returns ancestor chain including modules

Module Callback Methods

Callback When Called Parameters Description
included(base) Module included in class/module base (Class/Module) Hook for include operations
extended(object) Module extends object object (Object) Hook for extend operations
prepended(base) Module prepended to class base (Class/Module) Hook for prepend operations

Constant Resolution Rules

Context Resolution Order Example
Nested Module Inner → Outer → Top-level A::B::C searches C, then B, then A, then top-level
Class Method Class → Superclasses → Modules → Top-level Instance method constant lookup
Top-level Current scope → Top-level Global constant access

Module Inclusion Effects

Operation Method Location Lookup Chain Position Visibility
include Instance methods After including class Inherits module visibility
extend Singleton methods Object's singleton class Inherits module visibility
prepend Instance methods Before including class Inherits module visibility

Visibility Modifiers in Modules

Modifier Scope Effect on Including Class Example
public Default Methods become public Accessible from anywhere
private Module only Methods become private Only callable with send
protected Same object/class Methods become protected Accessible within class hierarchy

Common Module Patterns

Pattern Implementation Use Case
Mixin include ModuleName Add instance functionality
Class Extension extend ModuleName Add class methods
Namespace Module::Class Organize related classes
Callback Hook def self.included(base) Dynamic class modification
Method Interception prepend with super Wrap existing methods

Error Types

Error Cause Resolution
NameError: uninitialized constant Constant not found in scope Check constant definition and scope
NoMethodError: private method called Calling private module method Use send or make method public
ArgumentError: cyclic include detected Module includes itself Remove circular inclusion
TypeError: wrong argument type Invalid module argument Pass module object to include/extend

Module Inspection Methods

Method Returns Description
#name String Module name or nil for anonymous
#const_defined?(name) Boolean Whether constant exists in module
#const_get(name) Object Value of named constant
#const_set(name, value) Object Sets constant value
#constants Array Array of constant names
#method_defined?(name) Boolean Whether instance method exists
#private_method_defined?(name) Boolean Whether private method exists