Ruby 入门指南 / 第 09 章:面向对象编程
第 09 章:面向对象编程
“在 Ruby 中,一切都是对象。” —— 松本行弘
9.1 类与对象
9.1.1 定义类
class User
# 构造方法
def initialize(name, email)
@name = name
@email = email
@created_at = Time.now
end
# 实例方法
def info
"#{@name} (#{@email})"
end
def age_in_years
# 计算逻辑...
end
end
# 创建对象
user = User.new("Alice", "[email protected]")
puts user.info # => "Alice ([email protected])"
9.1.2 实例变量与访问器
class User
# 手动定义访问器
def name
@name
end
def name=(new_name)
@name = new_name
end
def email
@email
end
def email=(new_email)
@email = new_email
end
end
# 使用 attr 快捷方式(推荐)
class User
attr_reader :name, :email # 只读访问器
attr_writer :name # 只写访问器
attr_accessor :age # 读写访问器
def initialize(name, email)
@name = name
@email = email
end
end
user = User.new("Alice", "[email protected]")
user.name # => "Alice"(读)
user.name = "Bob" # 写(需要 attr_writer 或 attr_accessor)
user.age = 25 # 读写(attr_accessor)
9.1.3 实例方法 vs 类方法
class Calculator
# 类方法(通过 self. 定义)
def self.description
"A simple calculator"
end
# 另一种定义类方法的方式
class << self
def create_with_defaults
new(0, 0)
end
end
# 实例方法
def initialize(a, b)
@a = a
@b = b
end
def add
@a + @b
end
def subtract
@a - @b
end
end
# 调用方式
Calculator.description # 类方法
calc = Calculator.new(10, 5) # 实例方法
calc.add # => 15
Calculator.create_with_defaults # 类方法
9.1.4 类变量与类实例变量
class Counter
# 类变量(所有实例和子类共享)
@@total = 0
# 类实例变量(只属于当前类)
@class_count = 0
def initialize
@@total += 1
end
def self.total
@@total
end
def self.class_count
@class_count
end
def self.increment_class_count
@class_count += 1
end
end
Counter.new
Counter.new
Counter.total # => 2
Counter.increment_class_count
Counter.class_count # => 1
💡 最佳实践:尽量避免使用类变量(@@),推荐使用类实例变量配合类方法。
9.1.5 常量
class User
MAX_NAME_LENGTH = 50
ROLES = %i[admin editor viewer].freeze
attr_reader :name, :role
def initialize(name, role = :viewer)
raise ArgumentError, "名字过长" if name.length > MAX_NAME_LENGTH
raise ArgumentError, "无效角色" unless ROLES.include?(role)
@name = name
@role = role
end
end
# 访问常量
User::MAX_NAME_LENGTH # => 50
User::ROLES # => [:admin, :editor, :viewer]
# 嵌套常量
module Payment
class Gateway
TIMEOUT = 30
MAX_RETRIES = 3
end
end
Payment::Gateway::TIMEOUT # => 30
9.2 继承
9.2.1 单继承
class Animal
attr_reader :name
def initialize(name)
@name = name
end
def speak
raise NotImplementedError, "#{self.class} 必须实现 speak 方法"
end
def to_s
"#{self.class}: #{@name}"
end
end
class Dog < Animal
def speak
"Woof! I'm #{@name}"
end
def fetch(item)
"#{@name} fetches the #{item}"
end
end
class Cat < Animal
def speak
"Meow! I'm #{@name}"
end
def purr
"#{@name} purrs..."
end
end
dog = Dog.new("Buddy")
dog.speak # => "Woof! I'm Buddy"
dog.fetch("ball") # => "Buddy fetches the ball"
dog.to_s # => "Dog: Buddy"
cat = Cat.new("Whiskers")
cat.speak # => "Meow! I'm Whiskers"
9.2.2 super 关键字
class Vehicle
attr_reader :make, :model, :year
def initialize(make, model, year)
@make = make
@model = model
@year = year
end
def info
"#{@year} #{@make} #{@model}"
end
end
class Car < Vehicle
attr_reader :doors
def initialize(make, model, year, doors = 4)
super(make, model, year) # 调用父类构造方法
@doors = doors
end
def info
"#{super} (#{@doors}-door)" # 调用父类方法
end
end
car = Car.new("Toyota", "Camry", 2024)
car.info # => "2024 Toyota Camry (4-door)"
9.2.3 方法查找链
# Ruby 查找方法的顺序:
# 1. 当前类的实例方法
# 2. 混入的模块(后混入的先查找)
# 3. 父类
# 4. 父类的模块
# 5. BasicObject
class A
def greet
"Hello from A"
end
end
class B < A
def greet
"#{super} and B"
end
end
class C < B
def greet
"#{super} and C"
end
end
C.new.greet # => "Hello from A and B and C"
# 查看方法查找链
C.ancestors # => [C, B, A, Object, Kernel, BasicObject]
9.2.4 类型检查
dog = Dog.new("Rex")
dog.is_a?(Dog) # => true
dog.is_a?(Animal) # => true
dog.is_a?(Object) # => true
dog.instance_of?(Dog) # => true
dog.instance_of?(Animal) # => false(instance_of? 不检查父类)
# 响应能力检查(鸭子类型)
dog.respond_to?(:speak) # => true
dog.respond_to?(:fly) # => false
9.3 模块(Module)
9.3.1 模块作为命名空间
module Geometry
class Point
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def distance_to(other)
Math.sqrt((@x - other.x)**2 + (@y - other.y)**2)
end
end
class Circle
attr_reader :center, :radius
def initialize(center, radius)
@center = center
@radius = radius
end
def area
Math::PI * @radius**2
end
def circumference
2 * Math::PI * @radius
end
end
end
# 使用命名空间
p1 = Geometry::Point.new(0, 0)
p2 = Geometry::Point.new(3, 4)
p1.distance_to(p2) # => 5.0
circle = Geometry::Circle.new(p1, 5)
circle.area # => 78.53981633974483
9.3.2 Mixin:include 和 extend
# include - 将模块方法混入为实例方法
module Printable
def print
puts to_s
end
def print_with_border
puts "=" * 40
puts to_s
puts "=" * 40
end
end
class Report
include Printable
def initialize(title)
@title = title
end
def to_s
"Report: #{@title}"
end
end
report = Report.new("Sales Report")
report.print # => Report: Sales Report
report.print_with_border # 带边框输出
# extend - 将模块方法混入为类方法
module ClassInfo
def class_name
name
end
def description
"#{name} - #{ancestors.length} ancestors"
end
end
class MyClass
extend ClassInfo
end
MyClass.class_name # => "MyClass"
MyClass.description # => "MyClass - 6 ancestors"
9.3.3 经典 Mixin 模式
# Comparable 混入
class Temperature
include Comparable
attr_reader :value
def initialize(value)
@value = value
end
def <=>(other)
@value <=> other.value
end
def to_s
"#{@value}°C"
end
end
t1 = Temperature.new(20)
t2 = Temperature.new(30)
t3 = Temperature.new(25)
t1 < t2 # => true
t1 > t2 # => false
[t1, t2, t3].sort # => [20°C, 25°C, 30°C]
[t1, t2, t3].min # => 20°C
t1.between?(t3, t2) # => false(20 不在 25 和 30 之间)
# Enumerable 混入
class Playlist
include Enumerable
def initialize
@songs = []
end
def add(song)
@songs << song
end
def each(&block)
@songs.each(&block)
end
end
playlist = Playlist.new
playlist.add({ title: "Song A", duration: 180 })
playlist.add({ title: "Song B", duration: 240 })
playlist.add({ title: "Song C", duration: 200 })
playlist.map { |s| s[:title] } # => ["Song A", "Song B", "Song C"]
playlist.select { |s| s[:duration] > 200 } # => [{title: "Song B", duration: 240}]
playlist.sum { |s| s[:duration] } # => 620
9.3.4 多重 Mixin
module Loggable
def log(message)
puts "[#{Time.now}] #{self.class}: #{message}"
end
end
module Serializable
def to_json
require "json"
instance_variables.each_with_object({}) do |var, hash|
key = var.to_s.delete("@")
hash[key] = instance_variable_get(var)
end.to_json
end
def to_yaml
require "yaml"
instance_variables.each_with_object({}) do |var, hash|
key = var.to_s.delete("@")
hash[key] = instance_variable_get(var)
end.to_yaml
end
end
module Cacheable
def cache_key
"#{self.class.name.downcase}/#{id}"
end
def cached?
# 检查缓存逻辑
end
end
class User
include Loggable
include Serializable
include Cacheable
attr_reader :id, :name, :email
def initialize(id, name, email)
@id = id
@name = name
@email = email
end
end
user = User.new(1, "Alice", "[email protected]")
user.log("Created") # [2024-01-15 10:30:00] User: Created
user.to_json # => {"id":1,"name":"Alice","email":"[email protected]"}
user.cache_key # => "user/1"
9.4 开放类(Open Class)
9.4.1 猴子补丁(Monkey Patching)
# Ruby 允许重新打开任何类并添加/修改方法
# 这称为"猴子补丁"
# 为 String 添加方法
class String
def palindrome?
cleaned = downcase.gsub(/\s+/, "")
cleaned == cleaned.reverse
end
def word_count
split(/\s+/).length
end
def truncate(length, omission = "...")
return self if self.length <= length
self[0...(length - omission.length)] + omission
end
end
"racecar".palindrome? # => true
"hello world".word_count # => 2
"这是一个很长的字符串".truncate(5) # => "这是一个很..."
9.4.2 安全的猴子补丁
# ⚠️ 猴子补丁的风险
# - 可能与其他 gem 冲突
# - 可能破坏原有行为
# - 难以追踪和调试
# ✅ 更安全的方式:使用 refine(Ruby 2.0+)
module StringExtensions
refine String do
def word_count
split(/\s+/).length
end
def palindrome?
cleaned = downcase.gsub(/\s+/, "")
cleaned == cleaned.reverse
end
end
end
class MyProcessor
using StringExtensions
def process(text)
text.word_count # 只在当前类中生效
end
end
processor = MyProcessor.new
processor.process("hello world") # => 2
# String 实例不能直接使用 word_count
# "hello world".word_count # => NoMethodError
9.5 访问控制深入
9.5.1 protected 方法
class User
def initialize(name, score)
@name = name
@score = score
end
protected
def score
@score
end
public
def higher_score_than?(other)
@score > other.score # protected 方法可以被同类对象调用
end
end
alice = User.new("Alice", 95)
bob = User.new("Bob", 87)
alice.higher_score_than?(bob) # => true
# alice.score # => NoMethodError(不能直接调用 protected 方法)
9.5.2 private 方法
class Order
def initialize(items)
@items = items
end
def total
subtotal - discount + tax
end
private
def subtotal
@items.sum { |item| item[:price] * item[:quantity] }
end
def discount
subtotal > 100 ? subtotal * 0.1 : 0
end
def tax
subtotal * 0.08
end
end
order = Order.new([
{ price: 50, quantity: 2 },
{ price: 30, quantity: 1 }
])
order.total # => 118.8
# order.tax # => NoMethodError
9.6 初始化模式
9.6.1 Builder 模式
class UserBuilder
def initialize
@user = { name: nil, email: nil, age: nil, role: :viewer }
end
def name(name)
@user[:name] = name
self
end
def email(email)
@user[:email] = email
self
end
def age(age)
@user[:age] = age
self
end
def role(role)
@user[:role] = role
self
end
def build
User.new(@user[:name], @user[:email], @user[:age], @user[:role])
end
end
user = UserBuilder.new
.name("Alice")
.email("[email protected]")
.age(25)
.role(:admin)
.build
9.6.2 工厂方法
class Shape
def self.create(type, **options)
case type
when :circle
Circle.new(options[:radius])
when :rectangle
Rectangle.new(options[:width], options[:height])
when :triangle
Triangle.new(options[:a], options[:b], options[:c])
else
raise ArgumentError, "Unknown shape: #{type}"
end
end
end
class Circle
attr_reader :radius
def initialize(radius)
@radius = radius
end
def area
Math::PI * @radius**2
end
end
circle = Shape.create(:circle, radius: 5)
circle.area # => 78.53981633974483
9.7 实际业务场景
9.7.1 策略模式
# 价格计算策略
class PriceCalculator
attr_reader :strategy
def initialize(strategy)
@strategy = strategy
end
def calculate(price)
@strategy.calculate(price)
end
end
module Pricing
class Regular
def calculate(price)
price
end
end
class Member
def initialize(discount_rate = 0.1)
@discount_rate = discount_rate
end
def calculate(price)
price * (1 - @discount_rate)
end
end
class VIP
def calculate(price)
price * 0.8
end
end
end
# 使用
regular = PriceCalculator.new(Pricing::Regular.new)
member = PriceCalculator.new(Pricing::Member.new(0.15))
vip = PriceCalculator.new(Pricing::VIP.new)
puts regular.calculate(100) # => 100
puts member.calculate(100) # => 85.0
puts vip.calculate(100) # => 80.0
9.7.2 观察者模式
module Observable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def observers
@observers ||= []
end
def add_observer(observer)
observers << observer
end
end
def notify_observers(event, data = {})
self.class.observers.each do |observer|
observer.update(event, data) if observer.respond_to?(:update)
end
end
end
class Order
include Observable
attr_reader :status
def initialize
@status = :pending
end
def pay!
@status = :paid
notify_observers(:order_paid, { order: self })
end
def ship!
@status = :shipped
notify_observers(:order_shipped, { order: self })
end
end
class EmailNotifier
def update(event, data)
puts "Email: Order #{event}"
end
end
class InventoryManager
def update(event, data)
puts "Inventory: Processing #{event}"
end
end
order = Order.new
Order.add_observer(EmailNotifier.new)
Order.add_observer(InventoryManager.new)
order.pay!
# Email: Order order_paid
# Inventory: Processing order_paid
9.8 动手练习
- 实现一个简单的 ORM 基类
class Model
# 实现 find、save、delete 等基础方法
# 你的代码...
end
- 实现对象克隆和冻结
# 深拷贝一个包含嵌套对象的数据结构
# 然后冻结它,确保不可修改
- 实现 Mixin 方法追踪
# 创建一个模块,当被 include 时,自动记录所有方法调用
9.9 本章小结
| 要点 | 说明 |
|---|---|
| 类 | 使用 class...end 定义,支持实例变量和方法 |
| 属性 | attr_reader、attr_writer、attr_accessor |
| 继承 | Ruby 单继承,使用 < 语法 |
| 模块 | 命名空间和 Mixin 的载体 |
| Mixin | include 添加实例方法,extend 添加类方法 |
| 开放类 | 可以重新打开任何类,推荐使用 refine |
| 方法链 | ancestors 查看继承链 |
📖 扩展阅读
- Ruby 类文档
- Ruby 模块文档
- 《Object-Oriented Programming in Ruby》— Sandi Metz 著
- 《Practical Object-Oriented Design in Ruby》— Sandi Metz 著
上一章:← 第 08 章:字符串与正则 下一章:第 10 章:块与迭代器 →