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 から継承する Car と Motorcycle クラスをクリエイトします。これらは自動的にメーカー、モデル、エンジン始動のビヘイビアを獲得し、あなたはそれぞれに特化した drive や ride ビヘイビアを追加するだけで済みます。
もし後日 Truck(トラック)クラスを導入する場合も、同様に Vehicle から継承させることで、すべての乗り物タイプ間での一貫性(Consistency)を確保できます。
3.2 デジタルアセット管理
さまざまなデジタルアセット(Digital Asset)をマネジメントするシステムを考えてみましょう。すべての DigitalAsset オブジェクトは、ファイル名(filename)、サイズ(size)、作成日(creation_date)を持っているかもしれません。しかし、画像(Image)アセットは解像度(resolution)とディスプレイ(display)メソッドをさらに持ち、音声ファイル(AudioFile)は再生時間(duration)とプレイ(play)メソッドを持つ可能性があります。
Image と AudioFile を DigitalAsset のサブクラスとして継承させることで、すべてのアセットがベーシックな特徴をシェアすることを保証しつつ、異なるアセットタイプに対する特定の機能の実装を可能にし、コードの重複を回避できます。
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はスーパークラスです。DogとCatはサブクラスであり、どちらもAnimalから継承しています。DogとCatは自動的にname、speciesプロパティ、およびintroduceメソッドを獲得しています。- さらに、それぞれが独自の
initializeメソッドをディファインし、speakメソッドをオーバーライドし、固有のメソッド(fetchやpurrなど)を追加しています。
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} が声を出しました。"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 の主な使用方法は3つあります:
super(括弧やパラメータなし): 現在のメソッドにパッシングされたすべてのパラメータを、そのままスーパークラスのメソッドにフォワード(転送)します。super()(空の括弧あり): スーパークラスのメソッドにパラメータを一切パッシングしません(現在のメソッドがパラメータをレシーブしていたとしても)。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 つのドアを持つ トヨタ カムリ をドライブしています。ここでは、Car の start_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 を使用してコードを記述しています。