大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
OOP 与 Mixin
这一课你将能做到
- 定义类、属性读写器(attr_accessor)、构造方法,理解继承与 super
- 用模块(Module)做命名空间和 Mixin,说清 include 与 extend 的区别
- 理解方法查找链(Method Resolution Order, MRO)和 singleton class,避免过度使用 Monkey Patching
为什么要学
Ruby 是纯面向对象语言——连整数和 nil 都是对象,每个对象都有类。Ruby 的 OOP 模型比 Java 更灵活:单继承 + Mixin(模块混入)替代了多重继承;对象有「singleton class」让方法只属于单个对象。Rails 的 ActiveRecord、Concern 全都是 Mixin 的应用。
人话:Ruby 的类是蓝图,对象是实例;模块是「功能包」,可以像插件一样混进类里。Java 用接口+组合,Ruby 用 include/extend 一步到位。
先建立图像
对象的方法查找路径(MRO)
my_dog.speak
│
▼
┌──────────────────────────────┐
│ 1. singleton class of my_dog │ ← 单个对象独有方法
│ 2. Dog class │ ← 本类定义
│ 3. Speakable module (include) │ ← 最后 include 的先找
│ 4. Animal class (父类) │
│ 5. Walkable module │
│ 6. Object class │
│ 7. Kernel module │ ← puts/p 都在这里
│ 8. BasicObject │ ← 一切类的根
└──────────────────────────────┘
用 my_dog.class.ancestors 可以直接看到这个链
跟着做
步骤 1:类、继承与 super
class Animal
attr_accessor :name, :age # 自动生成 name, name=, age, age= 四个方法
attr_reader :species # 只读
def initialize(name, age, species = "未知")
@name = name
@age = age
@species = species
end
def speak
"..."
end
def describe
"#{@name}(#{@species},#{@age} 岁)"
end
end
class Dog < Animal
def initialize(name, age)
super(name, age, "犬") # 调父类构造方法
@tricks = []
end
def speak
"汪汪!"
end
def learn(trick)
@tricks << trick
end
end
dog = Dog.new("旺财", 3)
dog.age = 4 # attr_writer
puts dog.describe # => 旺财(犬,4 岁)
puts dog.speak # => 汪汪!
dog.learn("握手")
puts dog.species # => 犬(attr_reader)
步骤 2:模块——命名空间
module Payments
class CreditCard
def charge(amount)
"信用卡扣款 #{amount} 元"
end
end
class Alipay
def charge(amount)
"支付宝扣款 #{amount} 元"
end
end
end
cc = Payments::CreditCard.new
puts cc.charge(99)
步骤 3:Mixin——include 与 extend
module Loggable
def log(message)
puts "[#{self.class}] #{message}"
end
end
module Serializable
def to_h
instance_variables.each_with_object({}) do |var, h|
h[var.to_s.delete("@")] = instance_variable_get(var)
end
end
end
class Order
include Loggable # 实例方法
include Serializable
extend Enumerable # 给类本身加方法(class methods)——演示用
def initialize(id, total)
@id = id
@total = total
end
def process
log("处理订单 #{@id},金额 #{@total}")
end
end
order = Order.new(1001, 299)
order.process
puts order.to_h.inspect # => {"id"=>1001, "total"=>299}
# include vs extend 的核心差异:
# include Loggable → log 是实例方法,order.log 可用,Order.log 不可用
# extend Loggable → log 是类方法,Order.log 可用,order.log 不可用
步骤 4:singleton class 与 ancestors
class Person; end
p1 = Person.new
p2 = Person.new
# 给单个对象加方法——只有 p1 有
def p1.special_greet
"我是特殊的"
end
puts p1.special_greet
# puts p2.special_greet # NoMethodError!
# 查看方法查找链
class Dog; end
module Bark; def bark; "woof"; end; end
class Dog; include Bark; end
puts Dog.ancestors.inspect
# => [Dog, Bark, Object, Kernel, BasicObject]
步骤 5:Monkey Patching——知道但少用
# 给核心类打补丁
class String
def shout
upcase + "!"
end
end
puts "hello".shout # => HELLO!
# 危险:全局生效,别的库依赖 String 原行为会崩。
# 安全替代:Refinements(Ruby 2.0+),只在特定作用域生效
module StringShout
refine String do
def shout
upcase + "!"
end
end
end
using StringShout
puts "hi".shout # => HI!
# 出了这个文件/模块,String 恢复原样
讲清楚要点
- attr_accessor 只是语法糖:
attr_accessor :name等价于手动写def name; @name; end和def name=(v); @name = v; end。没有 JVM 那种真正的字段——一切都是方法调用。 - include 把模块插到类后面:祖先链是
子类 → 模块(后 include 的先找)→ 父类 → ...。所以后 include 的模块会覆盖先 include 的同名方法。 - extend 给「类本身」加方法:
class Foo; extend M; end让 M 的方法成为 Foo 的类方法。Rails 的belongs_to、has_many就是这样定义的。 - super 不带括号 vs 带括号:
super把所有参数原样传给父类同名方法;super()不传任何参数;super(a, b)显式传指定参数。三种写法语义不同。 - singleton class 是每个对象的「影子类」:
def obj.method或class << obj都改的是它。Ruby 的类方法本质就是「类的 singleton class 上的实例方法」——理解了这点,所有 Rails 类方法魔法都能看穿。
你可能会懵的地方
-
为什么 Ruby 用 Mixin 不用多重继承? 多重继承有菱形问题(父类 B、C 都继承 A,D 继承 B、C 时 A 的状态算几份?)。Mixin 是线性插入祖先链,没有歧义。Java 8 的 default method 本质也是在解决这个问题。
-
self到底指向谁? 看上下文:类定义体里self是类本身(所以def self.foo定义类方法);实例方法里self是接收者对象;模块里self是模块。Ruby 的 self 随执行位置动态变化。 -
模块能不能实例化? 不能。
Module.new可以造匿名模块,但MyModule.new会报错。模块只是「方法和常量的容器」,必须混进类里才能用。 -
is_a?、instance_of?、kind_of?的区别?instance_of?严格判断类相等;is_a?(别名kind_of?)沿祖先链判断,包括父类和 include 的模块。Rails 里鸭子类型更常用respond_to?。 -
prepend 是什么? Ruby 2.0+ 的第三种混入方式。
prepend M把模块插到类的前面:祖先链变成M → 本类 → ...。用于做 AOP 式的包装(在方法前后加逻辑),比 alias_method_chain 干净。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
直接 @name 暴露给外部 | 外部无法访问 | 用 attr_accessor/reader/writer |
| 子类 initialize 忘了 super | 父类状态没初始化 | 子类构造里先 super(...) |
| include 期望加类方法 | include 只加实例方法 | 类方法用 extend,或 ActiveSupport::Concern |
| 滥用 Monkey Patching | 全局污染、冲突难查 | Refinements 或装饰器模式 |
super 与 super() 混用 | 参数意外传/不传 | 明确想传参就带括号 |
卡点急救
undefined method 'xxx' for nil:NilClass:变量没赋值或方法返回了 nil。用&.xxx或在调用前puts obj.inspect排查。- 方法没找到但明明定义了:检查是不是 include 写成了 extend(或反过来)。用
MyClass.ancestors看模块是否真在链上。 - super 报错 ArgumentError:父类同名方法参数个数不匹配。
super(...)显式指定参数。
面试常问
-
include、extend、prepend 的区别? include:模块方法成为实例方法,模块插到类后面;extend:模块方法成为类方法,加到 singleton class;prepend:模块插到类前面,方法查找先命中模块,常用于 AOP。用
ancestors可以验证。 -
Ruby 的类方法本质是什么? 类方法是「类对象的 singleton class 上的实例方法」。
def self.foo实际是class << self; def foo; end; end的语法糖。所以类方法也走方法查找链。 -
Ruby 如何实现多态? 鸭子类型(Duck Typing):「如果它走像鸭子、叫像鸭子,它就是鸭子」。Ruby 不关心对象的类,只关心它是否响应某个方法(
respond_to?)。同名方法在不同类各自实现即可。
自测三问
attr_accessor :name展开后等价于哪几个方法?- 祖先链
Dog.ancestors返回[Dog, Bark, Animal, Object, Kernel, BasicObject],方法查找顺序是什么? - include 和 extend 混用会怎样?如何给一个类同时加实例方法和类方法?
小结一句话
类是蓝图、模块是功能包、祖先链是方法查找地图——掌握了 include/extend/prepend 和 singleton class,Ruby OOP 就通了。
延伸
- Ruby Module 官方文档
- 下一课:元编程——define_method、method_missing 与 DSL 的魔法。