Ruby 模块

模块(Module)是 Ruby 解决"单继承局限"的钥匙。它有两个核心用途:命名空间(组织代码、避免命名冲突)和 mixin(把一组方法"塞"进类里,实现类似多继承的能力)。Ruby 标准库里 ComparableEnumerable 都是模块,会用它们能让你的类瞬间拥有几十个方法。

1. 模块作为命名空间

当代码变大、有多个 User VERSION 这类通用名字时,把它们包进模块,用 :: 访问:

# 模块作为命名空间:用 :: 访问内部
module MyApp
  VERSION = "1.0.0"

  class User
    def initialize(name); @name = name; end
    attr_reader :name
  end

  def self.bootstrap
    puts "MyApp #{VERSION} 启动"
  end
end

# 用 :: 访问模块内的常量与类
u = MyApp::User.new("Alice")
puts u.name             # Alice
MyApp.bootstrap         # MyApp 1.0.0 启动
puts MyApp::VERSION     # 1.0.0

# 好处:避免和别人的 User、VERSION 撞名
# Gem 命名约定:模块名 = gem 名(大驼峰),如 ActiveSupport、Net::HTTP

Ruby 的标准库几乎都是命名空间形式:Net::HTTPFile::UtilsTime。Gems 也遵循同样约定——ActiveRecord::BaseSidekiq::Worker

2. 模块作为 mixin

这是模块最有用的能力。把通用方法塞进模块,任何类都能 include 进去复用:

# 模块作为 mixin:把方法"塞"进类
module Greetable
  def greet
    "Hi, I'm #{name}"
  end
end

class User
  include Greetable     # 把 Greetable 的方法变成 User 的实例方法
  attr_reader :name
  def initialize(name); @name = name; end
end

User.new("Alice").greet    # Hi, I'm Alice

# extend:把方法变成类方法
module Loggable
  def log(msg); puts msg; end
end

class Service
  extend Loggable         # Service.log "..." (类方法)
end

Service.log("started")    # started

include 把方法变成实例方法,extend 把方法变成类方法。这两条用得最多。

3. include / extend / prepend 三兄弟

三种混入方式在方法查找链(ancestors)里的位置不同:

# include / extend / prepend 的查找顺序差异

# include:插入到"类之上"
#   子类 super → 父类 → ... → 模块
module M1; def m; "M1"; end; end
class A; include M1; def m; "A"; end; end
A.new.m        # "A"(类自己的优先)
A.ancestors    # [A, M1, Object, ...]

# prepend:插入到"类之前"(可以 wrap 类的方法)
module M2
  def m
    puts "before"
    super            # 调用被包裹的原始方法
    puts "after"
  end
end
class B; prepend M2; def m; puts "B"; end; end
B.new.m
# before
# B
# after
B.ancestors    # [M2, B, Object, ...](M2 在 B 之前!)

# extend:把模块方法加成类的"类方法"(不影响实例方法)

关键:prepend 把模块放在类之前,所以模块的同名方法能"包裹"类的方法(用 super 调原方法)。这是写日志、缓存、性能监控等"切面"代码的核心技巧,Rails 5+ 大量用它。

4. Comparable:实现一个方法,获得一堆

Ruby 最优雅的设计之一——只要你的类实现 <=>(spaceship 运算符)并 include Comparable,就免费获得 < > == between? clamp 等十几个方法:

# Comparable:只要实现 <=> (spaceship),就能用 < > == between? 等
class Length
  include Comparable

  attr_reader :meters

  def initialize(meters); @meters = meters; end

  # Comparable 要求你实现这个方法
  def <=>(other)
    meters <=> other.meters
  end
end

a = Length.new(5)
b = Length.new(10)

puts a < b          # true(Comparable 自动提供!)
puts a == b         # false
puts a.between?(1, 100)  # true
puts [b, a].sort    # [5m, 10m](自动用 <=> 排序)
puts a.clamp(Length.new(7), Length.new(20))  # 7(夹紧到范围)

5. Enumerable:集合的瑞士军刀

实现 each 并 include Enumerable,你的类就拥有 map select reduce min max sort group_by partition几十个方法。Array、Hash 都靠它:

# Enumerable:实现 each,就能用 map/select/reduce/min/max 等
class Team
  include Enumerable

  def initialize(members); @members = members; end

  # Enumerable 要求你实现 each
  def each(&block)
    @members.each(&block)
  end
end

team = Team.new(["Alice", "Bob", "Cathy"])

# 现在 team 自动拥有了几十个方法!
team.map(&:upcase)        # ["ALICE", "BOB", "CATHY"]
team.select { |n| n.length > 4 }  # ["Alice", "Cathy"]
team.reduce(:+)           # "AliceBobCathy"
team.min_by(&:length)     # "Bob"
team.sort                 # ["Alice", "Bob", "Cathy"]
team.group_by(&:length)   # {5=>["Alice", "Cathy"], 3=>["Bob"]}
team.tally                # {"Alice"=>1, "Bob"=>1, "Cathy"=>1}(计数)

Enumerable 还提供 lazy(惰性求值)、flat_mapchunk_whiletally(计数,2.7+)等高级方法。它是 Ruby 函数式编程的主力。

6. module_function:既是模块方法也是实例方法

# module_function:让模块方法既能当模块方法,又能当实例方法
module MathUtils
  def square(x); x * x; end
  module_function :square    # 现在 MathUtils.square(5) 也行
end

MathUtils.square(5)     # 5

class Calc
  include MathUtils      # include 后也能当实例方法用
end

Calc.new.square(6)      # 36

# 典型例子:Math 模块
Math.sqrt(16)           # 2.0
Math::PI                # 3.14159...

这是 C 风格"工具函数"的写法——既可以直接 Module.func 调用,也可以 include 后当实例方法用。标准库 Math 就是这么实现的。

小结

模块是 Ruby 组织代码与复用方法的核心工具。命名空间防冲突,mixin 实现多继承,include/extend/prepend 控制查找位置,Comparable/Enumerable 让你"实现一个方法,获得几十个"。下一篇看 Ruby 的包管理——Gems 与 Bundler。

← 上一篇 Ruby 类与面向对象

下一篇 Ruby Gems 与 Bundler

✈️💬