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", "alice@example.com")
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", "alice@example.com", 25)
user.to_hash # => {name: "Alice", email: "alice@example.com", 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", "alice@example.com")
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 动手练习
- 实现模块自动注册
# 实现一个模块,当被 include 时自动注册到全局注册表
module Registrable
# 你的代码...
end
- 实现命名空间隔离
# 确保不同命名空间下的同名类互不干扰
module A
class User; end
end
module B
class User; end
end
- 实现模块优先级
# 当多个模块定义相同方法时,控制方法的调用顺序
13.8 本章小结
| 要点 | 说明 |
|---|---|
| 命名空间 | 模块用于组织代码,避免命名冲突 |
| include | 混入实例方法 |
| extend | 添加类方法 |
| require | 加载文件(只加载一次) |
| load | 每次都加载文件 |
| autoload | 延迟加载,首次使用时才加载 |
| require_relative | 相对于当前文件路径加载 |
| Concern | Rails 风格的模块组织模式 |
📖 扩展阅读
上一章:← 第 12 章:异常处理 下一章:第 14 章:文件与数据 →