强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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 动手练习

  1. 实现一个简单的 ORM 基类
class Model
  # 实现 find、save、delete 等基础方法
  # 你的代码...
end
  1. 实现对象克隆和冻结
# 深拷贝一个包含嵌套对象的数据结构
# 然后冻结它,确保不可修改
  1. 实现 Mixin 方法追踪
# 创建一个模块,当被 include 时,自动记录所有方法调用

9.9 本章小结

要点说明
使用 class...end 定义,支持实例变量和方法
属性attr_readerattr_writerattr_accessor
继承Ruby 单继承,使用 < 语法
模块命名空间和 Mixin 的载体
Mixininclude 添加实例方法,extend 添加类方法
开放类可以重新打开任何类,推荐使用 refine
方法链ancestors 查看继承链

📖 扩展阅读


上一章← 第 08 章:字符串与正则 下一章第 10 章:块与迭代器 →