Ruby 零基础教程

Ruby 类的继承

继承(Inheritance)允许你基于一个现有的类来定义一个新类,并继承其属性和行为。这种强大的机制促进了代码复用,在类之间建立了清晰的层级关系,并使你的程序更加井然有序和易于维护。

通过理解继承,你能在代码中更准确地对现实世界中的关系进行建模,创建一个让“特殊对象”能共享“通用对象”特征的结构,从而彻底告别重复代码。

本章将基于你之前掌握的类、对象和方法的知识,深入探讨继承在 Ruby 中是如何运作的,它的优势是什么,以及如何有效地实现它来构建健壮且可扩展的应用程序。

1. 什么是继承?

从核心上讲,继承是关于创建一个类层级结构,其中一个类(子类派生类)可以从另一个类(父类超类)派生出属性和行为。这种关系通常被称为 “是一个 (is-a)” 关系。

例如,一只狗 (Dog) 是一个动物 (Animal),一辆汽车 (Car) 是一个交通工具 (Vehicle)。子类会继承其父类所有可公开访问的属性(实例变量)和方法,同时它还可以添加自己独有的属性和方法,甚至修改(重写)继承来的内容。

1.1 “是一个 (Is-A)” 关系

“是一个”关系对于正确应用继承至关重要。它意味着子类的实例完全可以被当作父类的实例来对待。

  • 一辆轿车 (Sedan) 是一辆汽车 (Car)。
  • 一辆汽车 (Car) 是一个交通工具 (Vehicle)。
  • 一名开发者 (Developer) 是一名员工 (Employee)。

如果“是一个”的关系不成立,那么继承可能就不是最合适的设计模式。例如,一个车轮 (Wheel) 不是一辆汽车 (Car);相反,是一辆汽车拥有 (has-a) 一个车轮。这暗示了一种“拥有”的关系,通常使用组合 (Composition)(即一个对象将另一个对象作为属性包含进来)来建模,而不是继承。

2. 继承的优势

在软件开发中,继承提供了几个显著的优势:

  • 代码复用 (Code Reusability): 这是最立竿见影的好处。与其在多个类中编写相同的代码,不如在父类中定义一次通用的属性和方法,所有子类就会自动继承它们。这减少了冗余,使代码库更小、更易于管理。
  • 可维护性 (Maintainability): 当你需要更新某个通用行为时,只需在父类中修改一次,所有子类都会自动反映出该更改。这极大地简化了维护工作并降低了出错的几率。
  • 可扩展性 (Extensibility): 继承使你的应用程序更容易扩展。你可以添加继承自现有类的新(专用)类,而无需修改现有的父类代码,这遵循了开闭原则 (Open/Closed Principle)(对扩展开放,对修改关闭)。
  • 多态基础 (Polymorphism): 虽然本章不会深入探讨多态(这是一个更高级的 OOP 概念),但继承为它奠定了基础。多态(意为“多种形态”)允许不同类的对象被当作公共父类的对象来处理。这意味着你可以编写操作父类类型的代码,而它能正确作用于任何子类,从而产生更灵活、更通用的代码。

3. 现实世界类比

3.1 交通工具系统

想象你正在设计一个管理不同类型交通工具的系统。所有交通工具都有一些共同特征:它们都有品牌 (make)型号 (model),并且都能启动引擎 (start_engine)。然而,汽车 (Car) 还可以驾驶 (drive),而摩托车 (Motorcycle) 可以骑行 (ride)

  1. 你可以创建一个 Vehicle(交通工具)类,定义通用属性和方法。
  2. 然后,创建继承自 VehicleCarMotorcycle 类。它们会自动获得品牌、型号和启动引擎的行为,你只需添加它们特定的 driveride 行为。
  3. 如果以后引入 Truck(卡车)类,它也继承自 Vehicle,从而确保所有交通工具类型的一致性。

3.2 数字资产管理

考虑一个管理各种数字资产的系统。所有 DigitalAsset(数字资产)对象可能都有 filename(文件名)、size(大小)和 creation_date(创建日期)。然而,Image(图像)资产可能还有 resolution(分辨率)和 display(显示)方法,而 AudioFile(音频文件)可能有 duration(时长)和 play(播放)方法。

通过让 ImageAudioFile 作为子类继承 DigitalAsset,既确保了所有资产共享基本特征,又允许针对不同资产类型实现特定功能,避免了代码重复。

4. Ruby 继承:语法与机制

在 Ruby 中,使用一个特殊的符号实现继承非常直截了当。Ruby 支持单继承 (Single Inheritance),这意味着一个类只能从一个直接父类继承。不过,你可以创建一个长长的继承链(A 继承 B,B 继承 C,依此类推)。

4.1 定义父类与子类

要定义一个继承自父类的子类,需要在子类名后面使用 < 符号,紧接着写上父类名。

# 父类 (Superclass)
class Animal
  attr_accessor :name, :species

  def initialize(name, species)
    @name = name
    @species = species
  end

  def speak
    "#{name} 发出了声音。"
  end

  def introduce
    "你好,我的名字是 #{name},我是一只 #{species}。"
  end
end

# 子类 (Child Class) 继承自 Animal
class Dog < Animal
  def initialize(name, breed)
    # 调用父类的 initialize 方法
    # 在这里,'Dog' 是狗对象的物种 (species)
    super(name, "Dog")
    @breed = breed # 添加一个专门针对 Dog 的新属性
  end

  # 狗有自己独特的发声方式
  def speak
    "汪汪!汪汪!"
  end

  def fetch(item)
    "#{name} 把 #{item} 捡回来了!"
  end

  def dog_info
    "#{name} 是一只 #{@breed}。"
  end
end

# Cat 的子类,同样继承自 Animal
class Cat < Animal
  def initialize(name, fur_color)
    super(name, "Cat")
    @fur_color = fur_color
  end

  # 猫也有自己独特的发声方式
  def speak
    "喵!"
  end

  def purr
    "#{name} 正在满足地打呼噜。"
  end
end

在这个例子中:

  • Animal 是父类。
  • DogCat 是子类,都继承自 Animal
  • DogCat 自动获得了 namespecies 属性以及 introduce 方法。
  • 它们还定义了各自的 initialize 方法、重写了 speak 方法,以及各自独有的方法(如 fetchpurr)。

5. Object 类:万物之祖

在 Ruby 中,你创建的每个类都隐式地继承自一个名为 Object 的特殊类。如果你没有显式地为你的类指定父类,Ruby 就会假设 Object 是它的父类。Object 提供了许多所有 Ruby 对象都可用的基础方法,例如 to_s(转为字符串)、inspect(详细对象表示)、class(获取对象类名)和 is_a?(检查类型)。

你可以使用 ancestors 方法来验证一个类的完整继承链:

puts Dog.ancestors.inspect # => [Dog, Animal, Object, Kernel, BasicObject]

这显示了完整的继承层次结构,包括 Kernel(提供基本功能的模块)和 BasicObject(Ruby 中所有对象的终极根节点)。

6. 方法重写 (Method Overriding)

当子类定义了一个与其父类中同名的方法时,就会发生方法重写。当在子类实例上调用该方法时,执行的是子类的版本,这有效地替换(覆盖)了父类的实现。

在我们的例子中:

  • Animal 有一个 speak 方法:"#{name} 发出了声音。"
  • Dog 重写了 speak"汪汪!汪汪!"
  • Cat 重写了 speak"喵!"
my_dog = Dog.new("Buddy", "金毛寻回犬")
my_cat = Cat.new("Whiskers", "三花猫")
generic_animal = Animal.new("Leo", "狮子")

puts my_dog.speak          # 输出: 汪汪!汪汪! (Dog 的方法)
puts my_cat.speak          # 输出: 喵! (Cat 的方法)
puts generic_animal.speak  # 输出: Leo 发出了声音。 (Animal 的方法)

7. 使用 super 关键字

通常,当你在子类中重写一个方法时,你仍然希望利用父类版本该方法中的一部分逻辑。这就是 super 关键字派上用场的地方。super 允许你调用直接父类中同名的方法

使用 super 主要有三种方式:

  1. super (无括号或参数): 将传递给当前方法的所有参数,原封不动地传递给父类的方法。
  2. super() (带空括号): 不向父类的方法传递任何参数(即使当前方法接收到了参数)。
  3. super(arg1, arg2, ...) (带特定参数): 将你指定的参数传递给父类的方法。

super 最常见的用例是在 initialize 方法中,特别是当子类需要在父类处理完基础设置后,再添加自己的初始化逻辑时。

class Vehicle
  attr_accessor :make, :model

  def initialize(make, model)
    @make = make
    @model = model
  end

  def start_engine
    "正在启动 #{make} #{model} 的引擎。"
  end
end

class Car < Vehicle
  attr_accessor :num_doors

  def initialize(make, model, num_doors)
    super(make, model) # 调用 Vehicle 的 initialize
    @num_doors = num_doors # 添加 Car 特有的属性
  end

  def start_engine
    # 调用 Vehicle 的 start_engine,然后添加汽车特有的逻辑
    super + " 汽车引擎发出轰鸣声。"
  end

  def drive
    "正在驾驶有 #{num_doors} 扇门的 #{make} #{model}。"
  end
end

my_car = Car.new("丰田", "凯美瑞", 4)
puts my_car.start_engine # 输出: 正在启动 丰田 凯美瑞 的引擎。 汽车引擎发出轰鸣声。
puts my_car.drive        # 输出: 正在驾驶有 4 扇门的 丰田 凯美瑞。

在这里,Carstart_engine 方法首先调用 super 执行父类的逻辑,然后追加了自己的特定消息。这是扩展功能而非完全替换功能的经典模式。

8. 实战应用:构建类层级

让我们应用继承的知识来构建两个不同的类层级结构,演示如何组织通用和特定行为。

8.1 案例 1:Animal 及其后代

# 父类: Animal
class Animal
  attr_reader :name, :age 

  def initialize(name, age)
    @name = name
    @age = age
  end

  def eat(food_item)
    "#{name} 正在吃 #{food_item}。"
  end

  def sleep
    "#{name} 正在睡觉。"
  end

  def description
    "#{name} 是一只 #{self.class},今年 #{age} 岁。"
  end
end

# 子类: Dog
class Dog < Animal
  attr_reader :breed # Dog 特有属性

  def initialize(name, age, breed)
    super(name, age) # 调用 Animal 的 initialize 设置 name 和 age
    @breed = breed
  end

  def speak
    "汪汪!汪汪!"
  end

  def fetch(item)
    "#{name} 热情地把 #{item} 捡了回来!"
  end

  # 重写 description 以包含品种
  def description
    super + " 它是一只 #{breed}。"
  end
end

# 子类: Cat
class Cat < Animal
  attr_reader :color # Cat 特有属性

  def initialize(name, age, color)
    super(name, age)
    @color = color
  end

  def speak
    "喵!"
  end

  def scratch(object)
    "#{name} 抓挠了 #{object}。"
  end

  # 重写 description 以包含毛色
  def description
    super + " 它有 #{color} 的毛发。"
  end
end

my_dog = Dog.new("Buddy", 5, "金毛寻回犬")
my_cat = Cat.new("Whiskers", 3, "三花猫")

puts my_dog.description     # 输出: Buddy 是一只 Dog,今年 5 岁。 它是一只 金毛寻回犬。
puts my_dog.speak           # 输出: 汪汪!汪汪!
puts my_dog.eat("狗粮")      # 输出: Buddy 正在吃 狗粮。

8.2 案例 2:Employee 及其专业化

我们来模拟一个简单的员工管理系统。

# 父类: Employee
class Employee
  attr_reader :id, :name, :salary

  def initialize(id, name, salary)
    @id = id
    @name = name
    @salary = salary
  end

  def display_info
    "工号: #{id}, 姓名: #{name}, 薪水: $#{'%.2f' % salary}"
  end

  def give_raise(amount)
    @salary += amount
    "#{name} 的新薪水是 $#{'%.2f' % salary}。"
  end
end

# 子类: Manager
class Manager < Employee
  attr_reader :department

  def initialize(id, name, salary, department)
    super(id, name, salary) 
    @department = department
  end

  def display_info
    super + ", 部门: #{department}"
  end

  def assign_task(employee, task)
    "经理 #{name} 把任务 '#{task}' 分配给了 #{employee.name}。"
  end
end

# 子类: Developer
class Developer < Employee
  attr_reader :programming_language

  def initialize(id, name, salary, language)
    super(id, name, salary) 
    @programming_language = language
  end

  def display_info
    super + ", 编程语言: #{programming_language}"
  end

  def write_code
    "#{name} 正在使用 #{programming_language} 编写代码。"
  end
end

manager = Manager.new(101, "Alice Smith", 90000, "市场部")
developer = Developer.new(201, "Bob Johnson", 80000, "Ruby")

puts manager.display_info          # 输出: 工号: 101, 姓名: Alice Smith, 薪水: $90000.00, 部门: 市场部
puts developer.display_info        # 输出: 工号: 201, 姓名: Bob Johnson, 薪水: $80000.00, 编程语言: Ruby
puts developer.write_code          # 输出: Bob Johnson 正在使用 Ruby 编写代码。