强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Ruby 入门指南 / 第 13 章:模块深入

第 13 章:模块深入

“模块是 Ruby 组织代码和实现复用的核心机制。”


13.1 模块作为命名空间

13.1.1 组织代码

# 使用模块避免命名冲突
module Payment
  class Gateway
    def charge(amount)
      puts "Charging $#{amount}"
    end
  end

  class Transaction
    attr_reader :amount, :status

    def initialize(amount)
      @amount = amount
      @status = :pending
    end

    def complete!
      @status = :completed
    end
  end
end

module Shipping
  class Gateway
    def ship(order)
      puts "Shipping order #{order}"
    end
  end

  class Transaction
    attr_reader :tracking_number

    def initialize(tracking_number)
      @tracking_number = tracking_number
    end
  end
end

# 使用命名空间
pg = Payment::Gateway.new
pg.charge(100)

sg = Shipping::Gateway.new
sg.ship("ORD-001")

13.1.2 嵌套命名空间

module MyApp
  module Models
    class User
      attr_reader :name, :email
      
      def initialize(name, email)
        @name = name
        @email = email
      end
    end

    class Post
      attr_reader :title, :content
      
      def initialize(title, content)
        @title = title
        @content = content
      end
    end
  end

  module Services
    class UserService
      def find(id)
        # 查找用户
      end

      def create(params)
        # 创建用户
      end
    end
  end

  module Controllers
    class UsersController
      def index
        users = Models::User.all
        # ...
      end
    end
  end
end

user = MyApp::Models::User.new("Alice", "[email protected]")

13.2 模块的 include 与 extend

13.2.1 include 混入实例方法

module Serializable
  def to_hash
    instance_variables.each_with_object({}) do |var, hash|
      key = var.to_s.delete("@").to_sym
      hash[key] = instance_variable_get(var)
    end
  end

  def to_json
    require "json"
    to_hash.to_json
  end

  def to_yaml
    require "yaml"
    to_hash.to_yaml
  end
end

module Validatable
  def valid?
    validate!
    true
  rescue ValidationError
    false
  end

  def validate!
    raise NotImplementedError
  end
end

class User
  include Serializable
  include Validatable

  attr_accessor :name, :email, :age

  def initialize(name, email, age)
    @name = name
    @email = email
    @age = age
  end

  def validate!
    raise ValidationError, "Name required" if @name.nil? || @name.empty?
    raise ValidationError, "Invalid email" unless @email&.include?("@")
  end
end

user = User.new("Alice", "[email protected]", 25)
user.to_hash  # => {name: "Alice", email: "[email protected]", age: 25}
user.valid?   # => true

13.2.2 extend 添加类方法

module ClassInfo
  def class_name
    name || "Anonymous"
  end

  def ancestor_chain
    ancestors.select { |a| a.is_a?(Class) }.map(&:name)
  end
end

module Countable
  def self.extended(base)
    base.instance_variable_set(:@count, 0)
  end

  def increment
    @count += 1
  end

  def count
    @count
  end
end

class User
  extend ClassInfo
  extend Countable

  def initialize
    self.class.increment
  end
end

User.class_name      # => "User"
User.ancestor_chain  # => ["User", "Object", "BasicObject"]

User.new
User.new
User.count           # => 2

13.2.3 included 和 extended 钩子

module Trackable
  def self.included(base)
    puts "#{self} included into #{base}"
    base.extend(ClassMethods)
    base.include(InstanceMethods)
  end

  module ClassMethods
    def track_attributes(*attrs)
      @tracked_attributes = attrs
    end

    def tracked_attributes
      @tracked_attributes || []
    end
  end

  module InstanceMethods
    def changes
      @changes ||= {}
    end

    def track_change(attr, old_val, new_val)
      changes[attr] = { from: old_val, to: new_val }
    end
  end
end

class User
  include Trackable
  track_attributes :name, :email

  attr_reader :name, :email

  def initialize(name, email)
    @name = name
    @email = email
  end

  def name=(new_name)
    track_change(:name, @name, new_name)
    @name = new_name
  end
end

user = User.new("Alice", "[email protected]")
user.name = "Bob"
user.changes  # => {name: {from: "Alice", to: "Bob"}}

13.3 加载机制

13.3.1 require

# require - 加载文件(只加载一次)
require "json"           # 加载标准库
require "net/http"       # 加载嵌套库
require "./my_lib"       # 加载相对路径
require_relative "my_lib" # 推荐:相对于当前文件

# require 会记录已加载的文件
$"  # 已加载文件列表($LOADED_FEATURES)

# 检查是否已加载
require "json"   # => true(第一次)
require "json"   # => false(已加载)

# 加载路径
$LOAD_PATH  # 搜索路径($:)
$LOAD_PATH.unshift("/my/custom/path")

13.3.2 load

# load - 每次都加载文件(用于开发时重新加载)
load "my_script.rb"        # 加载并执行
load "my_script.rb", true  # 包裹在匿名模块中(避免污染)

# load 与 require 的区别
# - require:只加载一次,用于库
# - load:每次都加载,用于配置、脚本

# 开发环境中的热重载
def reload!
  load "./config.rb"
  load "./models/user.rb"
  load "./services/payment_service.rb"
end

13.3.3 autoload

# autoload - 延迟加载(首次使用时才加载)
module MyApp
  autoload :User, "models/user"
  autoload :Post, "models/post"
  autoload :PaymentService, "services/payment_service"
end

# 当首次引用 MyApp::User 时,才会加载 models/user.rb
user = MyApp::User.new  # 此时加载文件

# Rails 的自动加载机制就基于 autoload
# Zeitwerk 是 Rails 6+ 使用的自动加载器

13.3.4 require_relative

# require_relative - 相对于当前文件路径加载
# 假设目录结构:
# lib/
#   my_app.rb
#   my_app/
#     user.rb
#     post.rb

# lib/my_app.rb
require_relative "my_app/user"
require_relative "my_app/post"

module MyApp
  # ...
end

# lib/my_app/user.rb
module MyApp
  class User
    # ...
  end
end

13.4 模块方法

13.4.1 模块级方法

module MathHelper
  # 模块方法(通过 self. 定义)
  def self.square(n)
    n ** 2
  end

  def self.cube(n)
    n ** 3
  end

  def self.factorial(n)
    (1..n).reduce(1, :*)
  end
end

MathHelper.square(5)     # => 25
MathHelper.factorial(5)  # => 120

13.4.2 混合使用

module Formattable
  # 实例方法
  def format
    raise NotImplementedError
  end

  def display
    puts format
  end

  # 模块方法
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def format_collection(items)
      items.map(&:format).join("\n")
    end
  end
end

class User
  include Formattable

  attr_reader :name

  def initialize(name)
    @name = name
  end

  def format
    "User: #{@name}"
  end
end

user = User.new("Alice")
user.display  # => "User: Alice"
User.format_collection([user])  # => "User: Alice"

13.5 模块的高级模式

13.5.1 Concern 模式(Rails 风格)

module Concern
  def self.extended(base)
    base.instance_variable_set(:@dependencies, [])
  end

  def included(base = nil, &block)
    if base
      # 直接 include
      super(base)
    else
      # 返回块供后续 include
      @included_block = block
    end
  end

  def append_features(base)
    @dependencies&.each { |dep| base.include(dep) }
    super
    base.class_eval(&@included_block) if @included_block
  end

  def depend_on(*modules)
    @dependencies = modules
  end
end

# 使用 Concern
module Serializable
  extend Concern

  def to_hash
    instance_variables.each_with_object({}) do |var, hash|
      hash[var.to_s.delete("@").to_sym] = instance_variable_get(var)
    end
  end
end

module Timestampable
  extend Concern
  depend_on Serializable

  included do
    attr_reader :created_at, :updated_at
  end

  def touch
    @updated_at = Time.now
  end
end

class User
  include Timestampable  # 自动 include Serializable

  attr_reader :name

  def initialize(name)
    @name = name
    @created_at = Time.now
    @updated_at = Time.now
  end
end

user = User.new("Alice")
user.to_hash
user.touch

13.5.2 模块工厂

module Validations
  def self.validates(*attributes, **options)
    Module.new do
      define_method(:validate!) do
        attributes.each do |attr|
          value = send(attr)
          
          if options[:presence] && (value.nil? || value.to_s.empty?)
            raise "#{attr} is required"
          end

          if options[:numericality] && !value.is_a?(Numeric)
            raise "#{attr} must be numeric"
          end

          if options[:inclusion] && !options[:inclusion].include?(value)
            raise "#{attr} must be one of #{options[:inclusion]}"
          end
        end
      end

      define_method(:valid?) do
        validate!
        true
      rescue
        false
      end
    end
  end
end

class User
  include Validations.validates(:name, presence: true)
  include Validations.validates(:age, numericality: true)

  attr_accessor :name, :age
end

13.6 实际业务场景

13.6.1 插件系统

module PluginSystem
  class Registry
    def initialize
      @plugins = {}
    end

    def register(name, &block)
      @plugins[name] = block
    end

    def load(name, *args)
      plugin = @plugins[name]
      raise "Plugin not found: #{name}" unless plugin
      plugin.call(*args)
    end

    def available
      @plugins.keys
    end
  end
end

# 定义插件
registry = PluginSystem::Registry.new

registry.register(:logger) do |config|
  Module.new do
    define_method(:log) do |message|
      puts "[#{Time.now}] #{message}"
    end
  end
end

registry.register(:cache) do |config|
  Module.new do
    define_method(:cache) do |key, &block|
      @cache ||= {}
      @cache[key] ||= block.call
    end
  end
end

# 使用插件
class MyService
  include registry.load(:logger, level: :info)
  include registry.load(:cache, store: :memory)

  def process
    log("Processing...")
    cache("result") { expensive_calculation }
  end
end

13.6.2 中间件模式

module Middleware
  class Stack
    def initialize
      @middlewares = []
    end

    def use(middleware, **options)
      @middlewares << { klass: middleware, options: options }
    end

    def call(env)
      execute(0, env)
    end

    private

    def execute(index, env)
      return env if index >= @middlewares.length

      middleware = @middlewares[index][:klass]
      options = @middlewares[index][:options]

      middleware.new(-> (e) { execute(index + 1, e) }, **options).call(env)
    end
  end

  class Logging
    def initialize(app, **options)
      @app = app
      @options = options
    end

    def call(env)
      puts "[LOG] Request: #{env[:path]}"
      result = @app.call(env)
      puts "[LOG] Response: #{result[:status]}"
      result
    end
  end

  class Authentication
    def initialize(app, **options)
      @app = app
      @options = options
    end

    def call(env)
      unless env[:headers]&.key?("Authorization")
        return { status: 401, body: "Unauthorized" }
      end
      @app.call(env)
    end
  end
end

# 使用
stack = Middleware::Stack.new
stack.use(Middleware::Logging)
stack.use(Middleware::Authentication)

result = stack.call({
  path: "/api/users",
  headers: { "Authorization" => "Bearer token" }
})

13.7 动手练习

  1. 实现模块自动注册
# 实现一个模块,当被 include 时自动注册到全局注册表
module Registrable
  # 你的代码...
end
  1. 实现命名空间隔离
# 确保不同命名空间下的同名类互不干扰
module A
  class User; end
end

module B
  class User; end
end
  1. 实现模块优先级
# 当多个模块定义相同方法时,控制方法的调用顺序

13.8 本章小结

要点说明
命名空间模块用于组织代码,避免命名冲突
include混入实例方法
extend添加类方法
require加载文件(只加载一次)
load每次都加载文件
autoload延迟加载,首次使用时才加载
require_relative相对于当前文件路径加载
ConcernRails 风格的模块组织模式

📖 扩展阅读


上一章← 第 12 章:异常处理 下一章第 14 章:文件与数据 →