CrackedRuby logo

CrackedRuby

Module Methods

Overview

Module methods in Ruby encompass several distinct patterns for organizing and accessing functionality at the module level. Ruby modules can define methods directly on themselves, provide methods to classes through inclusion and extension, and expose functionality through various method visibility patterns.

Ruby implements module methods through three primary mechanisms. Module-level methods use self or module_function to create methods callable directly on the module. Modules can also define instance methods that become available to classes through include, or class methods through extend. Additionally, modules support nested method definitions and constant access patterns.

module Calculator
  def self.add(a, b)
    a + b
  end
  
  def multiply(a, b)
    a * b
  end
  module_function :multiply
end

Calculator.add(5, 3)      # => 8
Calculator.multiply(4, 2)  # => 8

The module method system provides namespace organization, code reuse, and mixin functionality. Ruby's method lookup chain resolves module methods based on inclusion order and method visibility. Modules cannot be instantiated directly, making their method access patterns distinct from class-based approaches.

module Authentication
  def self.hash_password(password)
    "hashed_#{password}"
  end
  
  def authenticate(user, password)
    user.password_hash == Authentication.hash_password(password)
  end
end

class User
  include Authentication
  attr_accessor :password_hash
end

user = User.new
user.authenticate(user, "secret")  # Instance method from module
Authentication.hash_password("secret")  # Module-level method

Module methods support both functional and object-oriented programming patterns. They enable utility functions, namespace organization, and controlled method exposure across Ruby applications.

Basic Usage

Module methods begin with defining methods directly on the module using self keyword or module_function directive. The self approach creates methods accessible only on the module itself, while module_function creates both private instance methods and public module methods.

module FileUtils
  def self.read_lines(filename)
    File.readlines(filename).map(&:chomp)
  end
  
  def copy_file(source, destination)
    File.write(destination, File.read(source))
  end
  module_function :copy_file
end

# Direct module method call
lines = FileUtils.read_lines("config.txt")

# Module function - can be called on module or included in classes
FileUtils.copy_file("source.txt", "dest.txt")

class FileManager
  include FileUtils
  
  def backup_config
    copy_file("config.txt", "config.backup")  # Available as instance method
  end
end

Method visibility in modules follows Ruby's standard private, protected, and public patterns. Private module methods require explicit receiver specification, while public methods can be called directly. The module_function directive automatically makes methods private when used as instance methods.

module Logger
  def self.info(message)
    puts "[INFO] #{message}"
  end
  
  def debug(message)
    puts "[DEBUG] #{timestamp} #{message}" if debug_enabled?
  end
  
  def timestamp
    Time.now.strftime("%Y-%m-%d %H:%M:%S")
  end
  
  module_function :debug
  private :timestamp
end

Logger.info("Application started")     # Public module method
Logger.debug("Processing request")     # Module function

class Service
  include Logger
  
  def process
    debug("Service processing")  # Available as instance method
  end
end

Modules support constant definition and access, providing namespace organization for both values and nested modules. Constants defined in modules can be accessed using the scope resolution operator or through module inclusion.

module Config
  DATABASE_URL = "postgresql://localhost/myapp"
  MAX_CONNECTIONS = 20
  
  module Cache
    TTL = 3600
    ENABLED = true
  end
  
  def self.database_config
    { url: DATABASE_URL, max_connections: MAX_CONNECTIONS }
  end
end

puts Config::DATABASE_URL           # => "postgresql://localhost/myapp"
puts Config::Cache::TTL             # => 3600
config = Config.database_config     # Method access to constants

Module inclusion and extension provide different method availability patterns. Including a module makes its instance methods available to the including class, while extending makes them available as class methods. This distinction enables flexible code organization patterns.

module Comparable
  def between?(min, max)
    self >= min && self <= max
  end
  
  def clamp(min, max)
    return min if self < min
    return max if self > max
    self
  end
end

class Temperature
  include Comparable
  
  attr_reader :celsius
  
  def initialize(celsius)
    @celsius = celsius
  end
  
  def <=>(other)
    celsius <=> other.celsius
  end
end

temp = Temperature.new(25)
puts temp.between?(Temperature.new(20), Temperature.new(30))  # => true
puts temp.clamp(Temperature.new(0), Temperature.new(40))      # Temperature object

Advanced Usage

Module methods support complex metaprogramming patterns through dynamic method definition and method aliasing. The define_method approach enables runtime method creation based on configuration or external data, while method aliasing provides backward compatibility and method decoration patterns.

module DynamicFinders
  def self.included(base)
    base.extend(ClassMethods)
  end
  
  module ClassMethods
    def create_finder(attribute)
      define_method("find_by_#{attribute}") do |value|
        instance_variables.map { |var| instance_variable_get(var) }
                          .find { |obj| obj.send(attribute) == value }
      end
      
      define_method("find_all_by_#{attribute}") do |value|
        instance_variables.map { |var| instance_variable_get(var) }
                          .select { |obj| obj.send(attribute) == value }
      end
    end
  end
end

class UserRepository
  include DynamicFinders
  
  create_finder(:email)
  create_finder(:status)
  
  def initialize
    @users = []
  end
  
  def add_user(user)
    @users << user
    instance_variable_set("@user_#{@users.length}", user)
  end
end

Module composition enables building complex functionality from smaller, focused modules. Ruby's method resolution order determines which methods take precedence when multiple modules define the same method name. This enables decorator and strategy patterns at the module level.

module Cacheable
  def cached_result(key)
    @cache ||= {}
    @cache[key] ||= yield
  end
  
  def clear_cache
    @cache = {}
  end
end

module Loggable
  def with_logging(operation)
    puts "Starting #{operation}"
    result = yield
    puts "Completed #{operation}"
    result
  end
end

module Measurable
  def measure_time
    start_time = Time.now
    result = yield
    end_time = Time.now
    puts "Execution time: #{end_time - start_time} seconds"
    result
  end
end

class DataProcessor
  include Cacheable
  include Loggable
  include Measurable
  
  def process_data(input)
    cached_result("process_#{input.hash}") do
      with_logging("data processing") do
        measure_time do
          # Expensive data processing operation
          sleep(1)
          input.upcase
        end
      end
    end
  end
end

Module methods can implement the Template Method pattern by defining algorithmic structure while allowing subclasses or including classes to provide specific implementations. This pattern combines module inclusion with method overriding for customizable behavior.

module Serializable
  def self.included(base)
    base.extend(ClassMethods)
  end
  
  def serialize
    {
      class: self.class.name,
      data: serialize_attributes,
      metadata: generate_metadata
    }.to_json
  end
  
  def serialize_attributes
    instance_variables.each_with_object({}) do |var, hash|
      attr_name = var.to_s.gsub('@', '')
      hash[attr_name] = instance_variable_get(var)
    end
  end
  
  def generate_metadata
    {
      serialized_at: Time.now.iso8601,
      version: self.class.serialization_version
    }
  end
  
  module ClassMethods
    def serialization_version
      "1.0"
    end
    
    def deserialize(json_string)
      data = JSON.parse(json_string)
      instance = new
      data['data'].each { |key, value| instance.send("#{key}=", value) }
      instance
    end
  end
end

class Product
  include Serializable
  
  attr_accessor :name, :price, :description
  
  def self.serialization_version
    "2.1"
  end
  
  def serialize_attributes
    # Custom serialization excluding sensitive data
    { 
      name: @name, 
      price: @price,
      description: @description&.truncate(100)
    }
  end
end

Module methods support callback and hook mechanisms through method interception and the method_added, included, and extended hooks. These patterns enable aspect-oriented programming and automatic configuration based on method definitions or module usage.

module Validatable
  def self.included(base)
    base.extend(ClassMethods)
    base.class_eval { @validations = [] }
  end
  
  def valid?
    self.class.validations.all? do |validation|
      case validation[:type]
      when :presence
        !send(validation[:attribute]).nil? && !send(validation[:attribute]).to_s.empty?
      when :length
        value = send(validation[:attribute]).to_s
        value.length >= validation[:min] && value.length <= validation[:max]
      when :format
        send(validation[:attribute]).to_s.match?(validation[:pattern])
      end
    end
  end
  
  def errors
    self.class.validations.filter_map do |validation|
      attribute = validation[:attribute]
      case validation[:type]
      when :presence
        "#{attribute} cannot be blank" if send(attribute).nil? || send(attribute).to_s.empty?
      when :length
        value = send(attribute).to_s
        if value.length < validation[:min]
          "#{attribute} must be at least #{validation[:min]} characters"
        elsif value.length > validation[:max]
          "#{attribute} must be at most #{validation[:max]} characters"
        end
      when :format
        "#{attribute} format is invalid" unless send(attribute).to_s.match?(validation[:pattern])
      end
    end
  end
  
  module ClassMethods
    attr_reader :validations
    
    def validates_presence_of(attribute)
      @validations << { type: :presence, attribute: attribute }
    end
    
    def validates_length_of(attribute, min:, max:)
      @validations << { type: :length, attribute: attribute, min: min, max: max }
    end
    
    def validates_format_of(attribute, with:)
      @validations << { type: :format, attribute: attribute, pattern: with }
    end
  end
end

Common Pitfalls

Module method visibility can create unexpected behavior when methods defined as module functions interact with private methods. Private methods in modules remain private when the module is included, but module functions become public when called on the module directly.

module Calculator
  def add(a, b)
    log_operation("add", a, b)
    a + b
  end
  
  def log_operation(op, *args)
    puts "Performing #{op} with #{args.join(', ')}"
  end
  
  module_function :add
  private :log_operation
end

# This works - log_operation is accessible within module context
Calculator.add(5, 3)  # => "Performing add with 5, 3" then 8

class MathService
  include Calculator
  
  def calculate
    add(2, 3)  # This fails! log_operation is private in instance context
  end
end

# Fix: Make log_operation a module function or use module-level logging
module BetterCalculator
  def add(a, b)
    BetterCalculator.log_operation("add", a, b)
    a + b
  end
  
  def self.log_operation(op, *args)
    puts "Performing #{op} with #{args.join(', ')}"
  end
  
  module_function :add
end

Constant lookup in modules follows Ruby's lexical scoping rules, but can produce unexpected results when constants are defined in including classes versus the module itself. Constants are resolved at the point of definition, not at the point of use.

module Configurable
  DEFAULT_TIMEOUT = 30
  
  def timeout_value
    DEFAULT_TIMEOUT  # Always resolves to module's constant
  end
  
  def current_timeout
    self.class::DEFAULT_TIMEOUT  # Attempts to resolve from including class
  end
end

class WebService
  include Configurable
  DEFAULT_TIMEOUT = 60  # Different value
end

class ApiClient
  include Configurable
  # No DEFAULT_TIMEOUT defined
end

web = WebService.new
api = ApiClient.new

web.timeout_value     # => 30 (module constant)
web.current_timeout   # => 60 (class constant)

api.timeout_value     # => 30 (module constant)
api.current_timeout   # => NameError: uninitialized constant ApiClient::DEFAULT_TIMEOUT

# Better approach: Use class-level configuration
module BetterConfigurable
  def self.included(base)
    base.extend(ClassMethods)
  end
  
  def timeout_value
    self.class.default_timeout
  end
  
  module ClassMethods
    def default_timeout
      defined?(self::DEFAULT_TIMEOUT) ? self::DEFAULT_TIMEOUT : 30
    end
  end
end

Method resolution order complications arise when multiple modules define the same method and are included in different orders. The last included module takes precedence, but this can create subtle bugs when module order changes.

module DatabaseLogging
  def save
    puts "Logging to database"
    super
  end
end

module FileLogging  
  def save
    puts "Logging to file"
    super
  end
end

module Caching
  def save
    puts "Updating cache"
    super
  end
end

class Document
  include DatabaseLogging
  include FileLogging
  include Caching
  
  def save
    puts "Saving document"
  end
end

# Method resolution order: Document -> Caching -> FileLogging -> DatabaseLogging
doc = Document.new
doc.save
# Output:
# Updating cache
# Logging to file  
# Logging to database
# Saving document

# Changing include order changes behavior
class ReorderedDocument
  include Caching
  include DatabaseLogging
  include FileLogging
  
  def save
    puts "Saving document"
  end
end

# Now FileLogging comes last and executes first
redoc = ReorderedDocument.new  
redoc.save
# Output:
# Logging to file
# Logging to database
# Updating cache
# Saving document

Module extension versus inclusion creates different method availability that can confuse developers. Extended methods become class methods, while included methods become instance methods. Mixing these patterns in the same module can create inconsistent interfaces.

module Utilities
  def format_currency(amount)
    "$%.2f" % amount
  end
  
  def self.format_currency(amount)
    "$%.2f" % amount
  end
end

class Product
  extend Utilities     # Methods become class methods
end

class Order
  include Utilities    # Methods become instance methods  
end

# Different access patterns for the same functionality
Product.format_currency(29.99)    # Works - class method
Order.new.format_currency(29.99)  # Works - instance method

# These don't work:
# Product.new.format_currency(29.99)  # NoMethodError
# Order.format_currency(29.99)        # NoMethodError

# Better approach: Design for both patterns
module BetterUtilities
  def format_currency(amount)
    BetterUtilities.format_currency(amount)
  end
  
  def self.format_currency(amount)
    "$%.2f" % amount
  end
  
  def self.included(base)
    base.extend(ClassMethods)
  end
  
  module ClassMethods
    def format_currency(amount)
      BetterUtilities.format_currency(amount)
    end
  end
end

Reference

Module Method Definition Patterns

Pattern Syntax Access Description
self.method def self.method_name Module.method_name Module-level method, callable only on module
module_function module_function :method_name Module.method_name or as instance method Creates both module and private instance method
Class method via extend extend ModuleName Class.method_name Module methods become class methods
Instance method via include include ModuleName instance.method_name Module methods become instance methods

Method Visibility in Modules

Visibility Module Call Instance Call Inheritance
public Available Available Inherited as public
protected Available Available to same class Inherited as protected
private Available Not available Inherited as private
module_function Public Private Inherited as private

Module Hooks and Callbacks

Hook Triggered When Common Use Cases
included(base) Module is included Add class methods, initialize class variables
extended(base) Module is extended Configure extended class, add metadata
method_added(method_name) Method is defined Automatic registration, validation setup
prepended(base) Module is prepended Override methods while preserving original

Constant Resolution Patterns

# Constant access patterns
module Constants
  VALUE = "module_value"
  
  def self.get_value
    VALUE                    # Module's own constant
  end
  
  def get_class_value
    self.class::VALUE       # Including class constant
  end
  
  def get_qualified_value  
    Constants::VALUE        # Fully qualified constant
  end
end

Method Resolution Order (MRO)

Ruby resolves methods in the following order:

  1. Singleton methods on the object
  2. Methods in prepended modules (reverse order of prepending)
  3. Methods in the object's class
  4. Methods in included modules (reverse order of inclusion)
  5. Methods in parent classes and their modules
  6. method_missing if defined

Common Module Patterns

Pattern Implementation Use Case
Namespace module Name; class X; end; end Group related classes
Mixin include ModuleName Share behavior across classes
Utility module Util; def self.method; end; end Stateless helper functions
Configuration module Config; SETTING = value; end Application settings
Template Abstract methods with raise NotImplementedError Define algorithms with customizable steps

Error Types and Module Methods

Error Cause Solution
NoMethodError Calling undefined module method Check method definition and visibility
NameError Undefined constant reference Verify constant exists in expected scope
ArgumentError Wrong number of method arguments Check method signature
TypeError Module used where class expected Use appropriate include/extend pattern
LoadError Module file not found Check require/load statements

Module Method Performance Characteristics

Operation Complexity Notes
Module method call O(1) Direct method lookup
Constant access O(1) Cached after first access
Method resolution O(n) Where n is ancestry chain length
Module inclusion O(1) Updates ancestor chain
Dynamic method definition O(1) Runtime method creation