Refinements, Hooks and Advanced Modules

26.1 Refinements (Scoped Monkey Patching)

module StringExtensions
  refine String do
    def palindrome?
      clean = downcase.gsub(/[^a-z0-9]/, "")
      clean == clean.reverse
    end
  end
end

# "racecar".palindrome?   # NoMethodError here
using StringExtensions     # active from here to the end of the file/module
"Racecar".palindrome?      # true

26.2 Hook Methods

self.included(base) / self.extended(base) / self.prepended(base) A module is mixed in

A class is subclassed (plugin registries) method_added / singleton_method_added / method_removed Methods are defined/removed

An undefined constant is referenced (autoloading) method_missing / respond_to_missing? An undefined method is called

initialize_copy / initialize_dup / initialize_clone An object is copied Numeric operations with your type

at_exit, ObjectSpace.define_finalizer Program exit / object GC

class Plugin
  @registry = []
  class << self
    attr_reader :registry
    def inherited(subclass)
      super
      Plugin.registry << subclass
    end
  end
end
class CsvExporter < Plugin; end
class JsonExporter < Plugin; end
Plugin.registry          # [CsvExporter, JsonExporter]

26.3 ActiveSupport::Concern Style (Plain Ruby)

module Auditable
  def self.included(base)
    base.extend(ClassMethods)
    base.instance_variable_set(:@audited, [])
  end

  module ClassMethods
    def audit(*fields) = @audited.concat(fields)
    def audited_fields = @audited
  end

  def audit_log = self.class.audited_fields.to_h { [it, public_send(it)] }
end

26.4 Autoloading and Code Loading

Zeitwerk (the gem used by Rails) autoloads constants from file names based on conventions.

require "json"                  # load a library/gem once
require_relative "lib/user"     # relative to the current file
load "script.rb"                # load every time
autoload :Parser, "parser"      # lazy load on first constant reference
$LOAD_PATH.unshift File.expand_path("lib", __dir__)