Ruby 模块(Module)与 Mixin 详解:include、extend、prepend 的区别

模块是 Ruby 组织代码与实现"多重继承"的核心机制。本文讲清模块的命名空间与 Mixin 两大用途,并用实例对比 include、extend、prepend 三种混入方式的方法查找顺序。

最佳实践
红宝石与应用开发工作台插画

Ruby 模块有两大用途:当命名空间(Admin::User)组织代码、当 Mixin 在类间共享行为。三种混入方式的区别在于方法查找链中的位置:include 插入类的后面(实例方法)、extend 加到类自身(类方法)、prepend 插到类的前面(可覆盖并 super 回调)。

模块是什么

模块和类一样是方法与常量的容器,但不能实例化。这个限制换来两大能力:组织命名空间和跨类复用代码。

module Payments
  class CreditCard; end
  class Alipay; end
end

Payments::CreditCard.new   # 命名空间用法

Rails 项目里 app/models/admin/user.rb 对应 Admin::User,模块就是避免类名冲突的隔离墙。

Mixin:Ruby 的"多重继承"

Ruby 类只能单继承,但行为复用不受限——把公共行为写进模块,需要的类 include 进来:

module Taggable
  def tags
    @tags ||= []
  end

  def add_tag(tag)
    tags << tag unless tags.include?(tag)
  end
end

class Article
  include Taggable
end

class Product
  include Taggable
end

Article 和 Product 毫无继承关系,却共享了同一套标签行为。Ruby 标准库的 Enumerable、Comparable 都是这个模式的典范。

include vs extend vs prepend

三者的区别在于模块进入方法查找链的位置:

module Greeter
  def hello
    '来自模块的问候'
  end
end

class A
  include Greeter   # 实例方法:A.new.hello 可用
end

class B
  extend Greeter    # 类方法:B.hello 可用
end

class C
  prepend Greeter   # 实例方法,但模块排在类"前面"
  def hello
    '来自类的问候'
  end
end

C.new.hello  # => '来自模块的问候'(prepend 的模块优先)
  • include:模块插在类和父类之间 → A.ancestors 为 [A, Greeter, Object, ...];
  • extend:模块插到类的单例类上 → 方法变成类方法;
  • prepend:模块插在类之前 → 模块方法覆盖类方法,且模块内可以 super 调回类的实现。

prepend 的独特价值:无痕增强

prepend + super 是实现"环绕增强"的干净方式,不需要 alias_method 链:

module Logging
  def save
    puts "保存前记录日志"
    super
  end
end

class Document
  prepend Logging
  def save
    puts '真正的保存'
  end
end

Document.new.save
# => 保存前记录日志
# => 真正的保存

Rails 的不少 instrumentation 和第三方 gem 的猴补丁都用这个机制——比直接改写原方法安全,因为原始实现通过 super 依然可达。

模块里的常量与方法可见性

模块可以嵌套定义常量(module Api; TIMEOUT = 5; end,用 Api::TIMEOUT 访问)。模块内的 private/protected 与类中语义一致;想让模块方法"既能被 include 又能直接以模块名调用",用 module_function(典型例子:Math.sqrt 与 include Math 后直接用 sqrt)。

常见问题(FAQ)

Q:什么场景用模块而不是继承?

行为是"能力"而非"身份"时用模块:一个类"是"什么用继承表达(Dog < Animal),"能"什么用 Mixin 表达(Taggable、Searchable、Cacheable)。类之间有 is-a 关系才继承,否则优先组合与 Mixin。

Q:include 多个模块方法名冲突怎么办?

后 include 的模块在查找链中更靠前,覆盖先 include 的同名方法。显式顺序:include A; include B 时 B 优先。依赖顺序的冲突是设计坏味道,最好改名或用 prepend 明确叠加关系。

Q:Concern(ActiveSupport::Concern)和直接 include 有什么区别?

Concern 解决了两个问题:依赖模块的包含顺序自动化(included 块里的 has_many 等类级调用按正确时机执行)、ClassMethods 子模块自动 extend。Rails 项目写可复用行为时优先用 Concern,纯 Ruby 项目普通模块即可。


参考文档

本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。

延伸阅读

获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

在线开通,按量计费,真正的云服务!

立即开始

选择观测云版本

代码托管平台