Rubyのclass入門(第2回): 継承・`include`/`extend`/`prepend`・メソッド探索順を整理する

はじめに

システム開発第一事業部の奥田です。普段はフルスタックエンジニアとしてWebアプリの開発を担当しています。

前回の記事では、class本体がその場で実行されること、selfが場面ごとに変わること、new -> allocate -> initializeの流れを整理しました。

今回はその続きとして、継承、include/extend/prepend、メソッド探索順を順番に見ていきます。

もともとは定数スコープ・可視性までを扱う予定だったのですが、結構ボリュームが多くなってしまったので分割することにしました。

この記事のゴール

  • 継承を「実装のコピー」ではなく「メソッド探索経路の追加」として説明できる
  • superを「親クラス固定」ではなく「探索順で次に見つかる実装を呼ぶ仕組み」として説明できる
  • include/extend/prependの違いを「どこにmoduleを差し込むか」で説明できる
  • ancestors/singleton_class.ancestorsを使って、実際の呼び出し先を特定できる
  • class A; class B; end; endclass A::BModule.nestingがどう変わるかを説明できる

検証環境

  • 実行確認: Ruby 3.3.11
  • 仕様確認: Ruby公式ドキュメント 3.3系

結論

忙しい人向けに記事の内容をまとめたものを置いておきます!

  • 継承は「親クラスをメソッド探索チェーンに追加する仕組み」で、子クラスで同名メソッドを定義すると子側が優先される
  • superは「親クラスを直接呼ぶ命令」ではなく「探索順で次に見つかる実装」を呼ぶ
  • includeはインスタンスメソッド探索にmoduleを入れ、extendは対象オブジェクトの特異クラス側にmoduleを入れる
  • prependはクラス本体より先にmoduleを置くため、既存メソッドの前後に処理を差し込める
  • 呼び出し先が不明なときは、インスタンスメソッドはancestors、クラスメソッドはsingleton_class.ancestorsを確認すると特定できる
  • ネスト定義(class A; class B; end; end)と定数パス定義(class A::B)はModule.nestingの結果が異なり、定数解決に影響する

継承はメソッド探索チェーンを作る

クラスは<を使うことで継承できます。

継承において継承元のクラスのことをsuperclass(スーパークラス、親クラス)とも言います。

継承先になるクラスのことをsubclass(サブクラス、派生クラス)とも言います。

class Child < Parent
end

であればParentがスーパークラス、Childがサブクラスというわけです。

ちなみにですが、前回の記事において

class User
  class << self
    def table_name
      "users"
    end
  end
end

p User.table_name

# "users"

という特異クラスの説明の中で<<というのも出てきているので少しややこしいですが、

  • <: 継承元(スーパークラス)を指定する構文
  • <<: 特異クラス(singleton class)を開く構文

というのをしっかりと押さえておきましょう!
(ちなみに<<はArrayなどのa << bといった演算子としても出てきます。ややこしいですね。)

Rubyのclassは多重継承できないため、親クラスは常に1つです。

もし、複数の振る舞いを合成(多重継承)したいときは、moduleinclude/prependして探索経路に組み込みます。
(後の章で説明していきます!)

そして、Rubyにおける継承とは「親クラスをメソッド探索経路に組み込む仕組み」です。
少し難しいですが、コードで確認していけばその意味がわかります!

class User
  VERSION = "1.0"

  def role
    "user"
  end

  def version
    VERSION
  end
end

class Admin < User
  def role
    "admin"
  end
end

p Admin.new.role
p Admin.new.version


# admin
# 1.0

この例を「どこを順番に探しているか」で説明すると、次のとおりです。

  1. Admin.new.roleを呼ぶ
  2. まずAdminroleがあるか探す
  3. Adminroleがあるので、それを実行して"admin"になる

次にAdmin.new.versionは次の順番です。

  1. まずAdminversionがあるか探す
  2. Adminにはないので、次にUserを探す
  3. Userversionがあるので、その実装を実行する

このように、継承は「親クラスの実装をコピーする機能」ではなく、「メソッドを探す順番に親クラスを追加する機能」です。

同じ名前のメソッドが子クラス側にあれば、探索がそこで止まるため、子クラスの実装が優先されます。

さらに、親の実装を使い回したいときはsuperを使います。

class User
  def label(name)
    "User: #{name}"
  end
end

class Admin < User
  def label(name)
    "Admin -> #{super(name)}"
  end
end

puts Admin.new.label("tom")

# Admin -> User: tom

superは「親クラスを呼ぶ」と覚えがちですが、正確には「今の位置の次に見つかる実装」を呼びます。

include/extend/prependは何が違うのか

Railsのコードを読み始めると、この3つ(include/extend/prepend)がよく出てくるかと思います。

言葉の意味合いは似ていますが、役割ははっきり分かれています。
重要なポイントはmoduleのメソッドをどこへ置くか」ということです。

この点を踏まえると3つのメソッドは以下のように説明できます。

  • include: そのクラスのインスタンスメソッドの探索経路(ancestors)へmoduleを追加する
  • extend: そのオブジェクトのsingleton class(特異クラス)の探索経路へmoduleを追加する
  • prepend: moduleをクラス本体より先に探される位置へ入れる

ところで「moduleってなんだ...?」と思った方もいるのではないでしょうか?

というわけなので、moduleの説明を先にしていきます!

classmoduleは用途を分けて使う

classmoduleはどちらもメソッドや定数を持てますが、用途が違います。

  • class: インスタンスを作るためのもの。newでオブジェクトを作り、<で継承できる
  • module: それ単体ではnewできない。主な用途はmixinnamespace

ここでmixinnamespaceについて確認していきましょう!

賢くなった気になれる補足: mixinnamespace

mixinnamespaceの意味は

  • mixin: moduleに共通メソッドを定義し、include/prependで複数クラスへ取り込む使い方
  • namespace: moduleの内側へクラスや定数を置き、A::Bの形で名前の衝突を防ぐ使い方

です。

mixinは「共通機能を混ぜ込む」ための使い方です。
継承のような親子関係を作らず、必要なメソッドだけを複数クラスで再利用できます。

Rubyの命名の慣習では、mixin用のmodule~able(例: LoggableTrackable)で表す書き方がよく使われます。

module Loggable
  def log(message)
    "[LOG] #{message}"
  end
end

class User
  include Loggable
end

class Admin
  include Loggable
end

p User.new.log("create")
p Admin.new.log("delete")

# [LOG] create
# [LOG] delete

このコードで起きていることは次の3点です。

  1. UserAdmininclude Loggableを実行した時点で、Loggablelogが両クラスのインスタンスメソッド探索経路に入る
  2. User.new.log("create")を呼ぶと、User本体にlogがないためLoggable#logが呼ばれる
  3. Admin.new.log("delete")でも同じ探索が行われ、同じLoggable#logが呼ばれる

つまり、mixin「共通メソッドを1か所に置いて、複数クラスで同じ実装を使う」ための仕組みというわけですね!

一方で、namespaceは「名前を整理する箱」です。
::で区切ることで、同名クラスがあっても衝突せずに使い分けできます。

class User
end

module Billing
  TAX_RATE = 0.1

  class Invoice
    def tax_for(amount)
      amount * TAX_RATE
    end
  end
end

module Shipping
  class Invoice
    def label
      "shipping invoice"
    end
  end
end

p User.new.class
p Billing::Invoice.new.tax_for(1000)
p Shipping::Invoice.new.label

# User
# 100.0
# shipping invoice

この例では

  1. Billingnamespaceとして働きます。
  2. InvoiceBilling::Invoiceという名前で区別できるので、同じInvoiceという名前がほかにあっても衝突しにくくなります。
  3. また、Billing配下の定数TAX_RATEも同じ箱(= Billingが持つ定数のスコープ)にまとめて管理できます。

つまり、namespace同名のクラスや定数を衝突させずに管理する仕組みです。

定数解決の細かい動きは、次回記事のModule.nestingの章で確認します。
(少々お待ちください!)

classmoduleの違いをコードで確認

classmoduleの違いをサンプルコードで確認していきましょう。

class User
end

module Billing
end

# `class`はインスタンス化できる
p User.new.class

# `module`自体はインスタンス化できない
begin
  Billing.new
rescue NoMethodError => e
  puts e.class
  puts e.message
end

class Admin < User
end

# `class`は別の`class`を継承できる
p Admin.superclass


# User
# NoMethodError
# undefined method `new' for Billing:Module
# User

このコードで確認できる違いは次の2点です。

  1. classnewでインスタンス化できる
  2. modulenewできないため、インスタンスを直接作る器ではない

そのため、実務では「オブジェクトを作る本体」はclassに書き、共通機能の再利用や名前整理はmoduleに分けます。

続いて、moduleのインスタンスメソッドとモジュールメソッドの違いを確認します。

  • moduleのインスタンスメソッドは、includeしたクラスのインスタンスから呼ぶメソッドです。
  • moduleのモジュールメソッドは、AccessRule.method_nameのようにmodule名を付けて直接呼ぶメソッドです。

そして、重要なポイントはmodule本体でdef method_nameselfなしで書くと、 そのmoduleのインスタンスメソッドとして定義されるということです!

moduleのインスタンスメソッドとして定義されるため、include先のクラスインスタンスから呼べるというわけですね。

ではコードを確認してみましょう!

module AccessRule
  # インスタンスメソッド
  def instance_rule
    "instance rule"
  end

  # ここでの`self`は`AccessRule`自身を指すため、`def self.xxx`で
  # `AccessRule.xxx`の形で直接呼べるモジュールメソッドを定義できる
  # モジュールメソッド
  def self.module_rule
    "module rule"
  end
end

class User
  include AccessRule
end

p User.new.instance_rule
p AccessRule.module_rule

# module_ruleはモジュールメソッドなので失敗する
begin
  User.module_rule
rescue NoMethodError => e
  puts e.class
end

# instance_ruleはインスタンスメソッドなので失敗する
begin
  AccessRule.instance_rule
rescue NoMethodError => e
  puts e.class
end

# "instance rule"
# "module rule"
# NoMethodError
# NoMethodError

この結果から次のことを確認できます。

  1. includeUserへ入るのはinstance_rule(インスタンスメソッド)
  2. module_rule(モジュールメソッド)はUserへ入らないため、User.module_ruleは失敗する
  3. module_ruleAccessRule.module_ruleのように、moduleをレシーバにして直接呼べる

includeはインスタンスメソッドを混ぜる

moduleの説明の中でも出てきたincludeですが、includeはインスタンスメソッドを混ぜる役割を持っています。

module Greeting
  def hello
    "hello"
  end
end

class User
  include Greeting
end

p User.new.hello

# "hello"
  • Greetinghelloは、Userから作ったインスタンスで使えるメソッドになります。
  • includeは「このクラスから作るオブジェクト全員に新しい機能を足す」と考えると理解しやすくなります。
  • GreetingUser.ancestorsに入り、インスタンスメソッド探索経路の一部になります。

ancestorsclassmoduleのスーパークラスとインクルードしているmoduleを優先順位順に配列に格納して返すメソッドです。

module Greeting
  def hello
    "hello"
  end
end

class User
  include Greeting
end

p User.ancestors

# [User, Greeting, Object, Kernel, BasicObject]

User.ancestorsの先頭付近を見ると、Userの次にGreetingが入っていることを確認できますね。

extendはオブジェクト専用メソッドを追加する

extendincludeとは違ってオブジェクト専用メソッドを追加する役割を持っています。

module Finder
  def recent
    "recent records"
  end
end

class User
  extend Finder
end

p User.recent

begin
  User.new.recent
rescue NoMethodError => e
  puts e.class
end

# "recent records"
# NoMethodError

このコードからわかることは以下の2点です。

  1. FinderのメソッドがUserのインスタンスに入ったのではなく、Userというクラスオブジェクト自身に追加される。
  2. そのためUser.new.recentではなくUser.recentでメソッドを呼び出す必要がある

User.new.recentの形式だとインスタンスメソッドの呼び出しなので、きっちり失敗しているのが確認できるかと思います。

また、正確には、FinderUsersingleton class(特異クラス)の探索経路に追加されているため、User.recentという形で呼び出す必要があります。

つまり冒頭でお伝えした「オブジェクト専用メソッドを追加する役割」というのは、

「そのオブジェクトの特異クラスにメソッドを追加する役割」ということです。

Rubyの「クラスメソッドっぽい機能」の多くは、「クラスオブジェクトに機能を足している」と見ると整理しやすくなります。

prependは探索順の先頭に入る

prependは「レシーバのインスタンスメソッド探索順」の先頭にmoduleを挿入します。
そのため、classに対してprependしたときは、まずインスタンスメソッドに効きます。

module Traceable
  def label
    "trace -> #{super}"
  end
end

class User
  prepend Traceable

  def label
    "user"
  end
end

p User.new.label

# "trace -> user"

このコードでは、User#labelより先にTraceable#labelが呼ばれます。

superで次の探索先(ここではUser#label)へ処理を渡せるため、前後に処理を挟めます。

一方で、クラスメソッド側に効かせたいときは、特異クラスへprependします。

module ClassTraceable
  def build
    "trace(class) -> #{super}"
  end
end

class User
  def self.build
    "user build"
  end
end

class << User
  prepend ClassTraceable
end

p User.build

# "trace(class) -> user build"

この2つの例からprependは次のように使い分けます。

  • 既存メソッドの前後で処理を挟みたい
  • インスタンスメソッドを差し替えたいならclassprepend
  • クラスメソッドを差し替えたいならclass << 対象prepend

少し難解に感じるかもしれませんが、その感覚、正解です!

prependと特異クラスはRubyの中でも読解難度が高い部類です...
意味を理解せずに使うとバグを量産する原因にもなります...

実務での使用頻度は次のとおりです(経験からの割合)

  • include: 高頻度(共通インスタンスメソッドの追加)
  • extend: 中〜高頻度(クラスメソッド追加、DSL(Domain Specific Language: ドメイン特化言語))
  • prepend: 低〜中頻度(既存メソッドを横取りして前後処理を挟むとき)

賢くなった気になれる補足: DSLってなに?

DSLは「Domain Specific Language(ドメイン特化言語)」と言います。

特定の用途だけを短く書けるようにした書き方を指します。
Ruby/Railsだと次のような記述がDSLです。

class User < ApplicationRecord
  has_many :posts
  validates :name, presence: true
end

Railsを触っているとたくさん出てきますね!

どのメソッドが呼ばれるかはancestorsで見る

「同じ名前のメソッドが複数あったとき、Rubyがどこから探すのか」は、この先のコード読解で外せないポイントです!

Ruby公式のcalling_methodsでは、Rubyは次の順番でメソッドを探すと説明されています。

The prepended modules of R in reverse order.
For a matching method in R.
The included modules of R in reverse order.

わかりやすくすると以下の流れになります。

  1. prependされたモジュール(新しいものが先頭)
  2. クラス自身
  3. includeされたモジュール(新しいものが先頭)
  4. スーパークラス側へ順に移動
  5. 同様のルールで再帰

ですが、これを目視で追っていくのは大変ですよね?

そんなときに便利なメソッドがancestorsです。
ancestorsを使うと、この順番をそのまま一覧で確認できます。

module M1
  def label
    "from M1"
  end
end

module M2
  def label
    "from M2"
  end
end

class Base
  include M1

  def label
    "from Base"
  end
end

class User < Base
  prepend M2
end

p User.ancestors
p User.new.label

# [M2, User, Base, M1, Object, Kernel, BasicObject]
# "from M2"

この順番を見ると、prependがクラス本体より前で探されることがわかります。

「なぜこのメソッドが呼ばれているのかわからない」となったら、まずancestorsを見るとかなり整理しやすくなります。

クラスメソッドの探索順はsingleton_class.ancestorsで見る

インスタンスメソッドだけでなく、クラスメソッドにおいても「どの順番で探すか」を確認できます。

ここではextendprependをクラスオブジェクト側に入れた例を見ます。

module ClassMethodsA
  def source_name
    "from A"
  end
end

module ClassMethodsB
  def source_name
    "from B"
  end
end

class User
  extend ClassMethodsA

  class << self
    prepend ClassMethodsB
  end
end

p User.singleton_class.ancestors.first(4)
p User.source_name

# [ClassMethodsB, #<Class:User>, ClassMethodsA, #<Class:Object>]
# "from B"

ここで見てほしいのは次の2点です。

  • extend ClassMethodsAしたモジュールは、Userのクラスメソッドを探す並びに入る
  • class << selfの中でprepend ClassMethodsBすると、その並びのさらに先頭に入る

RubyやRailsのソースを読んでいて「このクラスメソッドはどこから来たのか」がわからなくなったら、 User.singleton_class.ancestorsを見ると整理しやすくなります。

クラスをネストしたときの基本動作

Rubyのコードを見ているとclassがネストしているコードもよく見かけると思います。
このclassをネストしたときの動きについても確認していきましょう!

Rubyではクラス/モジュールのネスト情報はModule.nestingを使うことで確認できます。

Module.nestingは「このメソッドが呼び出された時点で、どのクラス/モジュールの入れ子にいるか」を配列で返します。

class Admin
  p self               # ①
  p Module.nesting     # ①

  class UserPolicy
    p self             # ②
    p Module.nesting   # ②
  end

  p self               # ③
  # 「今のクラス/モジュールに直接定義された定数名だけ」を配列で返す
  p constants(false)   # ③
end

p Admin::UserPolicy    # ④


# ①
# Admin
# [Admin]

# ②
# Admin::UserPolicy
# [Admin::UserPolicy, Admin]

# ③
# Admin
# [:UserPolicy]

# ④
# Admin::UserPolicy

上の4手順と出力の対応は次のとおりです。

  1. ①: Admin[Admin]が、class Admin本体の実行中であることを示す
  2. ②: Admin::UserPolicy[Admin::UserPolicy, Admin]が、内側クラス実行中であることを示す
  3. ③: Adminが内側クラスを抜けて外側へ戻ったことを示す
  4. ④: [:UserPolicy]Admin::UserPolicyが、UserPolicyAdminの定数として登録済みであることを示す

出力結果からわかるRubyの内部的な動作として、

  • class本体は上から順にその場で実行される(メソッドは実行されません)
  • 外側classの実行途中で内側class定義に到達したら、内側class本体もその場で実行される
  • 内側のclassの処理が終わると外側classの続きに戻る

ということがわかりましたね!

なお、defに関してはこの時点では実行されず、メソッド呼び出し時に実行されます。

そして、ネストしない形でもクラスを定義できます。
ただし、次のコードは見た目が似ていますが、さきほどのネスト構文とは少し意味が異なります。

class Admin
end

class Admin::UserPolicy
  p Module.nesting
end

# [Admin::UserPolicy]

class Admin::UserPolicyは、Admin名前空間の下にクラスを定義(または再オープン)する省略形です。

先ほどの

class Admin
  class UserPolicy
  end
end

とやっていることは同じです。

ですが、Module.nestingの結果を見比べてみると

  • class Admin class UserPolicy: [Admin::UserPolicy, Admin]
  • class Admin::UserPolicy: [Admin::UserPolicy]

となっており、この形式はModule.nestingAdminが入っていません。

これはclass Admin::UserPolicyは「Adminの中に入って書いている」構文ではなく、 Admin::UserPolicyという定数パスを直接指定してクラスを開く構文だからです。

そのためレキシカルスコープはAdminの内側にならず、Module.nestingが返す情報にAdminは入ってこないということになります。

この違いによって定数の動作が少し変わってくるのですが、 これに関しては次回記事で説明します!

一次情報: - Ruby公式:modules_and_classes(Nesting / Constants) - Ruby公式:Class

モジュールがネストしたときの動作

「あれ?モジュールの場合はどうなるの?」
と思われた方のためにモジュールのネストについても説明しておきます!

モジュールにおいても、クラスと同じ規則で動きます!

module Billing
  p self
  p Module.nesting

  module Tax
    p self
    p Module.nesting
  end

  p self
  p constants(false)
end

p Billing::Tax

# Billing
# [Billing]
# Billing::Tax
# [Billing::Tax, Billing]
# Billing
# [:Tax]
# Billing::Tax

この出力からわかることは次の4点です。

  1. module Billingに入ると、その時点のselfBillingになる
  2. module Taxに入ると、selfBilling::Taxに切り替わる
  3. module Taxを抜けると、selfBillingへ戻る
  4. 内側で定義したTaxは、Billingの定数として登録される(Billing::Tax

module Billing::Tax形式でも同じ名前は作れますが、Module.nestingは変わります。

module Billing
end

module Billing::Tax
  p Module.nesting
end

# [Billing::Tax]

module Billing::Tax形式ではModule.nestingBillingが入りません。

そのため、クラスのときと同様に定数の動作に違いが出てきます。

まとめ

今回の要点は次の5つです。

  • 継承は「親を探索順へ追加する仕組み」で、superは「次に見つかる実装」を呼ぶ
  • include/extend/prependは、moduleを差し込む場所がそれぞれ違う
  • インスタンスメソッドの呼び出し先はancestorsで確認できる
  • クラスメソッドの呼び出し先はsingleton_class.ancestorsで確認できる
  • ネスト定義と定数パス定義ではModule.nestingの結果が変わり、定数解決の挙動も変わる

次回(第3回)は、今回触れたModule.nestingの続きとして、定数スコープと可視性(public/private/protected)を整理します。

参考資料

テコテックの採用活動について

テコテックでは新卒・中途採用を積極的に行っています。 採用サイトでは会社の雰囲気や福利厚生、募集ポジションをご確認いただけます。 ご興味をお持ちいただけましたら、ぜひチェックしてみてください。 tecotec.co.jp