【Ruby】オブジェクト指向設計を噛み砕いていく(#5 継承を理解する)

プログラミング初心者の勉強ブログ #85

「オブジェクト指向設計」について理解を深めていくシリーズ。「オブジェクト指向設計実践ガイド」を読んでの備忘録になります。今回は「継承」の理解を深める回です。サブクラスとスーパークラスについて、実際のコードに沿って確認します。superメソッドや親クラスのオーバーライドについての曖昧な理解を整理していきます。

 

目次

[toc]

 

前回までの内容

[blogcard url=”https://whatsupguys.net/programming-school-dive-into-code-learng-81/”]

[blogcard url=”https://whatsupguys.net/programming-school-dive-into-code-learning-80/”]

[blogcard url=”https://whatsupguys.net/programming-school-dive-into-code-learning-78/”]

[blogcard url=”https://whatsupguys.net/programming-school-dive-into-code-learning-77/”]

 

オブジェクト指向における「継承」とは

継承とは、「メッセージの自動移譲」の仕組みそのものを指します。「メッセージを送る」はオブジェクト間のやり取りのことであり、オブジェクトが連携する際に用いられるメソッドやその引数が「メッセージ」という言葉で表されております。

※意味がしっくりこない場合は「前回までの内容」を読んで頂きたいです。

別の言い方だと、クラスは「親クラス(以下スーパークラス)」と「子クラス(以下サブクラス)」という上下の「階層構造」を用いることができ、サブクラスに何かしらのメソッドが当てられたとき、そのメソッドがサブクラスに定義されているメソッドに該当しなかった場合、そのサブクラスのスーパークラスに対し自動で該当しなかったメソッドを当てる仕組みが「継承」になります。

オブジェクト指向において継承を定義したクラス達は、「サブクラス→スーパークラス」の一方向性で、当てられたメソッドが定義されているクラスを見つけ出すまで遡り続けます。もちろん結果該当するメソッドが無かった場合エラーを出します。

ポイントは、

  1. 「サブクラス(具象)→スーパークラス(抽象)」の一方向性でメッセージを委譲する
  2. 該当するメソッドが見つかった場合、それ以上はメッセージの委譲をしない
  3. 単一継承による階層構造複雑化の回避

であり、理解する過程でinicializeメソッドのオーバーライドやsuperメソッドについても触れることができます。

 

「サブクラス→スーパークラス」の一方向性でメッセージを委譲する

継承によるメッセージの自動委譲は「子クラス」から「親クラス」へ遡ります。逆流しないことで抽象から具象への流れを正常に保ちます。

継承によるクラスの階層構造は、サブクラスであればある程「具体的」で「特殊」な特徴を含むクラスになります。逆にスーパークラスであればある程「抽象的」で「一般的」な事象のみを含んだクラスとして存在する必要があります。「抽象クラス」はサブクラスを作るためだけに存在します。

これは一般には「汎化ー特化の関係」と言われており、「具体的で特殊な特徴を含むサブクラス」から「抽象的で一般的な事象のみを含んだスーパークラス」へと遡ることで、コードの重複や不必要な依存を減らし、プログラムの再利用をしやすくすることに繋がります。

 

該当するメソッドが見つかった場合、それ以上はメッセージの委譲をしない

あるクラス(以下クラスA)に当てたメソッドがそのスーパークラス(以下クラスAA)で見つかった場合、その更にスーパークラス(以下クラスAAA)で同じ名前のメソッドがあったとしても、クラスAAで定義されたメソッドの中身でプログラムは動くことになります。

 

initializeメソッドの例

「initializeメソッド」を考えるとわかりやすいです。

initializeメソッドとは、クラスがインスタンス化された際の初期化メソッドです。インスタンス生成時にそのクラス内に定義されたinitializeメソッドが必ず動きます。このメソッドはどのクラスにおいても、

class AAA
 attr_reader :hogehoge, :fugafuga

 def initialize(args)
  @hogehoge = args[:hogehoge]
  @fugafuga = args[:fugafuga]
 end
end 

class AA < AAA
 attr_reader :hoge, :fuga

 def initialize(args)
   @hoge = args[:hoge]
   @fuga = args[:fuga]
 end
end



aa = AA.new(:hoge => "hoge",
            :fuga => "fuga",
            :hogehoge => "hogehoge",
            :fugafuga => "fugafuga")

aa.hoge
# -> "hoge"

aa.fuga
# -> "fuga"

aa.hogehoge
# -> nil

aa.fugafuga
# -> nil

のような形でメソッド名が「initialize」になるので、クラスAAで一旦initializeメソッドが動いた場合、もしクラスAAAでinitializeメソッドが別で定義されていたとしても、それは動きません。これはクラスAAがクラスAAAのinitializeメソッドをオーバーライドしている状態です。サブクラスがスーパークラスのメソッドを上書きしています。

ここでもし、クラスAAだけでなくクラスAAAのinitializeメソッドも動かしたい場合、「super」メソッドの出番です。

class AAA
 attr_reader :hogehoge, :fugafuga

 def initialize(args)
  @hogehoge = args[:hogehoge]
  @fugafuga = args[:fugafuga]
 end
end 

class AA < AAA
 attr_reader :hoge, :fuga

 def initialize(args)
  @hoge = args[:hoge]
  @fuga = args[:fuga]
  super(args)
 end
end


aa = AA.new(:hoge => "hoge",
            :fuga => "fuga",
            :hogehoge => "hogehoge",
            :fugafuga => "fugafuga")

aa.hoge
# -> "hoge"

aa.fuga
# -> "fuga"

aa.hogehoge
# -> "hogehoge"

aa.fugafuga
# -> "fugafuga"

クラスAAのinitializeメソッドに「super(args)」を加え、クラスAAAのinitializeメソッドにメッセージを委譲してます。こうすることでクラスAAをインスタンス化する際の「:hogehoge => “hogehoge”」と「:fugafuga => “fugafuga”」がしっかり「@hogehoge」と「@fugafuga」に格納され、「aa」から引っ張り出せます。

 

単一継承による階層構造複雑化の回避

単一継承とは、サブクラスは親となるスーパークラスを1つしか持たないことを指します。「サブクラス→スーパークラス」の一方向性によってメソッドを遡りコードの再利用を行う継承の構造において、一つのサブクラスにスーパークラスが複数存在すると、どちらの親にメッセージを委譲すれば良いかで問題が発生し、複雑な事態に陥ることになります。

Rubyにおける継承は、必ず「単一継承」の状態です。また、Rubyであるクラスを定義し、そのクラスのスーパークラスを定義しなかったら、自動で「Objectクラス」があるクラスのスーパークラスになります。

 

まとめ

継承とオーバーライド、initializeメソッドやsuperメソッドは、一つ一つで学んでいたときよりも、今回のようにまとめて整理した方がしっくりきました。めっちゃサブクラスとスーパークラスを使い分けて文章を書いたので、どっちがどっちでどのように継承されてるかごっちゃになってしまう状況からは脱せたと思います。

以上ありがとうございました。

 

返信を残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA