Ruby 入門

Ruby クラスの継承

継承(Inheritance)は、既存のクラスをベースに新しいクラスをディファイン(Define)し、そのプロパティ(Properties)とビヘイビア(Behaviors)を受け継ぐことを可能にする強力なメカニズムです。これにより、コードの再利用が促進され、クラス間に明確な階層関係が構築されることで、プログラムはよりオーガナイズ(Organize)され、メンテナンス性が飛躍的に向上します。

継承を深く理解することで、コード上で現実世界の関係性をより正確にモデリング(Modeling)できるようになります。「汎用的なオブジェクト」の特徴を「特化したオブジェクト」がシェアする構造をクリエイトすることで、冗長な重複コードから完全に脱却できます。

本章では、これまでに学習したクラス、オブジェクト、メソッドの知識をベースに、Rubyにおいて継承がどのように機能するのか、そのメリットは何か、そして堅牢(Robust)で拡張性(Extensibility)の高いアプリケーションをビルドするために、いかに効果的に実装すべきかをディープダイブ(Deep dive)して探求します。

1. 継承(Inheritance)とは?

コアな部分から言えば、継承とはクラスの階層構造(Hierarchy)をクリエイトすることです。あるクラス(サブクラス / Subclass、または派生クラス)は、別のクラス(スーパークラス / Superclass、または親クラス)からプロパティやビヘイビアを派生させることができます。この関係性は通常、「Is-A(〜は〜である)」関係と呼ばれます。

例えば、犬(Dog)は動物(Animal)であり(Is-A)、自動車(Car)は乗り物(Vehicle)です(Is-A)。サブクラスは、スーパークラスのすべてのパブリックアクセス可能なプロパティ(インスタンス変数)とメソッドを継承すると同時に、独自のアトリビュート(Attribute)やメソッドを追加したり、継承したコンテンツをモディファイ(オーバーライド / Override)したりすることができます。

1.1 "Is-A" 関係

「Is-A」関係は、継承を正しく適用するための極めて重要な基準です。これは、サブクラスのインスタンス(Instance)が、完全にスーパークラスのインスタンスとして扱えることを意味します。

  • セダン(Sedan)は、自動車(Car)である(Is-A)。
  • 自動車(Car)は、乗り物(Vehicle)である(Is-A)。
  • デベロッパー(Developer)は、従業員(Employee)である(Is-A)。

もし「Is-A」の関係が成立しない場合、継承は最適なデザインパターン(Design Pattern)ではない可能性が高いです。例えば、車輪(Wheel)は自動車(Car)ではありません。そうではなく、自動車は車輪を持っている(Has-A)という関係になります。これは「所有」の関係を示唆しており、通常は継承ではなくコンポジション(Composition:あるオブジェクトが別のオブジェクトをプロパティとして包含する手法)を用いてモデリングされます。

2. 継承のメリット

ソフトウェア開発において、継承はいくつかの顕著なアドバンテージを提供します。

  • コードの再利用性 (Code Reusability): これは最もダイレクトなメリットです。複数のクラスで同じコードを繰り返し記述する代わりに、スーパークラスで汎用的なプロパティとメソッドを一度だけディファインすれば、すべてのサブクラスがそれらを自動的に継承します。これにより冗長性が排除され、コードベースがよりスリムでマネジメントしやすくなります。
  • メンテナンス性 (Maintainability): ある汎用的なビヘイビアをアップデートする必要が生じた場合、スーパークラスを一度モディファイするだけで、すべてのサブクラスにその変更が自動的に反映されます。これにより、メンテナンス作業が劇的にシンプルになり、バグの発生確率を低下させます。
  • 拡張性 (Extensibility): 継承はアプリケーションのスケール(Scale)を容易にします。既存のスーパークラスのコードをモディファイすることなく、そこから継承した新しい(特化した)クラスをアド(Add)することができます。これはオープン・クローズドの原則(Open/Closed Principle:拡張に対しては開いており、修正に対しては閉じているべき)に準拠しています。
  • ポリモーフィズムの基盤 (Polymorphism): 本章ではポリモーフィズム(より高度なOOPコンセプト)には深く立ち入りませんが、継承はそのためのファウンデーション(Foundation)を築きます。ポリモーフィズム(「多様性」を意味します)は、異なるクラスのオブジェクトを、共通のスーパークラスのオブジェクトとして扱うことを可能にします。つまり、スーパークラスの型を操作するコードを記述すれば、それがどのサブクラスに対しても正しく作用し、よりフレキシブル(Flexible)で汎用的なコードを生み出すことができます。

3. 現実世界のアナロジー

3.1 乗り物(Vehicle)システム

さまざまなタイプの乗り物をマネジメントするシステムを設計していると想像してください。すべての乗り物には共通の特徴があります。メーカー(Make)、モデル(Model)があり、エンジンをスタートさせる(start_engine)ことができます。しかし、自動車(Car)はドライブする(drive)ことができ、オートバイ(Motorcycle)はライドする(ride)ことができます。

あなたは Vehicle(乗り物)クラスをクリエイトし、汎用的なプロパティとメソッドをディファインすることができます。
そして、Vehicle から継承する CarMotorcycle クラスをクリエイトします。これらは自動的にメーカー、モデル、エンジン始動のビヘイビアを獲得し、あなたはそれぞれに特化した driveride ビヘイビアを追加するだけで済みます。

もし後日 Truck(トラック)クラスを導入する場合も、同様に Vehicle から継承させることで、すべての乗り物タイプ間での一貫性(Consistency)を確保できます。

3.2 デジタルアセット管理

さまざまなデジタルアセット(Digital Asset)をマネジメントするシステムを考えてみましょう。すべての DigitalAsset オブジェクトは、ファイル名(filename)、サイズ(size)、作成日(creation_date)を持っているかもしれません。しかし、画像(Image)アセットは解像度(resolution)とディスプレイ(display)メソッドをさらに持ち、音声ファイル(AudioFile)は再生時間(duration)とプレイ(play)メソッドを持つ可能性があります。

ImageAudioFileDigitalAsset のサブクラスとして継承させることで、すべてのアセットがベーシックな特徴をシェアすることを保証しつつ、異なるアセットタイプに対する特定の機能の実装を可能にし、コードの重複を回避できます。

4. Ruby における継承:シンタックスとメカニズム

Rubyにおいて、特別なシンボル(Symbol)を使用して継承を実装するのは非常にストレートフォワード(Straightforward)です。Rubyは単一継承(Single Inheritance)をサポートしており、これは1つのクラスが直接継承できるスーパークラスは1つだけであることを意味します。ただし、長い継承チェーン(Inheritance Chain: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 クラス:すべてのルート(Root)

Rubyにおいて、あなたがクリエイトするすべてのクラスは、暗黙的に Object という特別なクラスから継承しています。クラスに対して明示的にスーパークラスを指定しなかった場合、Rubyは自動的に Object をそのスーパークラスとみなします。Object は、すべてのRubyオブジェクトで利用可能な多くのベーシックなメソッドを提供します。例えば、to_s(文字列への変換)、inspect(オブジェクトの詳細なリプレゼンテーション)、class(オブジェクトのクラス名の取得)、is_a?(タイプのチェック)などです。

ancestors メソッドを使用することで、クラスの完全な継承チェーンを検証(Verify)することができます。

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

これは完全な継承の階層構造を示しており、基本機能を提供するモジュールである Kernel や、Rubyにおけるすべてのオブジェクトの究極のルートノードである BasicObject も含まれています。

6. メソッドのオーバーライド

サブクラスがスーパークラスと同名のメソッドをディファインした際に、メソッドのオーバーライドが発生します。サブクラスのインスタンス上でそのメソッドがコールされた場合、サブクラスのバージョンがエクスキュート(Execute)され、これは実質的にスーパークラスの実装をリプレイス(上書き)することになります。

私たちのコード例では:

  • Animal には speak メソッドがあります: "#{@name} が声を出しました。"
  • Dogspeak をオーバーライドしています: "ワンワン!ワンワン!"
  • Catspeak をオーバーライドしています: "ニャー!"
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 の主な使用方法は3つあります:

  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 をコールしてスーパークラスのロジックを実行し、その後独自のメッセージをアペンド(Append)しています。これは機能を完全にリプレイスするのではなく、機能を拡張(Extend)するクラシックなパターンです。

8. 実践アプリケーション:クラス階層の構築

継承の知識を適用して、2つの異なるクラス階層構造をビルドし、汎用的なビヘイビアと特化したビヘイビアをどのようにオーガナイズするかをデモンストレーションしましょう。

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)マネジメントシステムをシミュレートしてみましょう。

# スーパークラス: Employee
class Employee
  attr_reader :id, :name, :salary

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

  def display_info
    "社員ID: #{@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          # 出力: 社員ID: 101, 氏名: Alice Smith, 給与: $90000.00, 部署: マーケティング部門
puts developer.display_info        # 出力: 社員ID: 201, 氏名: Bob Johnson, 給与: $80000.00, プログラミング言語: Ruby
puts developer.write_code          # 出力: Bob Johnson は Ruby を使用してコードを記述しています。