はじめに
システム開発第一事業部の奥田です。普段はフルスタックエンジニアとしてWebアプリの開発を担当しています。
前回の記事では、class本体がその場で実行されること、selfが場面ごとに変わること、new -> allocate -> initializeの流れを整理しました。
今回はその続きとして、継承、include/extend/prepend、メソッド探索順を順番に見ていきます。
もともとは定数スコープ・可視性までを扱う予定だったのですが、結構ボリュームが多くなってしまったので分割することにしました。
- 第1回(前回): Rubyのclass入門(前編): getter / setter・self・クラスメソッドの正体を突き止める
- 第2回(今回): 継承、
include/extend/prepend、メソッド探索順を扱う - 第3回(予定): 定数スコープ、可視性を扱う
- 第4回(予定): ここまでの知識を使って、Railsの
has_many、scope、コールバック、delegate、ActiveSupport::Concernの読み方を整理する
- はじめに
- この記事のゴール
- 検証環境
- 結論
- 継承はメソッド探索チェーンを作る
- include/extend/prependは何が違うのか
- どのメソッドが呼ばれるかはancestorsで見る
- クラスをネストしたときの基本動作
- まとめ
- 参考資料
- テコテックの採用活動について
この記事のゴール
- 継承を「実装のコピー」ではなく「メソッド探索経路の追加」として説明できる
superを「親クラス固定」ではなく「探索順で次に見つかる実装を呼ぶ仕組み」として説明できるinclude/extend/prependの違いを「どこにmoduleを差し込むか」で説明できるancestors/singleton_class.ancestorsを使って、実際の呼び出し先を特定できるclass A; class B; end; endとclass A::BでModule.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つです。
もし、複数の振る舞いを合成(多重継承)したいときは、moduleをinclude/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
この例を「どこを順番に探しているか」で説明すると、次のとおりです。
Admin.new.roleを呼ぶ- まず
Adminにroleがあるか探す Adminにroleがあるので、それを実行して"admin"になる
次にAdmin.new.versionは次の順番です。
- まず
Adminにversionがあるか探す Adminにはないので、次にUserを探すUserにversionがあるので、その実装を実行する
このように、継承は「親クラスの実装をコピーする機能」ではなく、「メソッドを探す順番に親クラスを追加する機能」です。
同じ名前のメソッドが子クラス側にあれば、探索がそこで止まるため、子クラスの実装が優先されます。
さらに、親の実装を使い回したいときは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の説明を先にしていきます!
classとmoduleは用途を分けて使う
classとmoduleはどちらもメソッドや定数を持てますが、用途が違います。
class: インスタンスを作るためのもの。newでオブジェクトを作り、<で継承できるmodule: それ単体ではnewできない。主な用途はmixinとnamespace
ここでmixinとnamespaceについて確認していきましょう!
賢くなった気になれる補足: mixinとnamespace
mixinとnamespaceの意味は
mixin:moduleに共通メソッドを定義し、include/prependで複数クラスへ取り込む使い方namespace:moduleの内側へクラスや定数を置き、A::Bの形で名前の衝突を防ぐ使い方
です。
mixinは「共通機能を混ぜ込む」ための使い方です。
継承のような親子関係を作らず、必要なメソッドだけを複数クラスで再利用できます。
Rubyの命名の慣習では、mixin用のmoduleを~able(例: Loggable、Trackable)で表す書き方がよく使われます。
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点です。
UserとAdminでinclude Loggableを実行した時点で、Loggableのlogが両クラスのインスタンスメソッド探索経路に入るUser.new.log("create")を呼ぶと、User本体にlogがないためLoggable#logが呼ばれる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
この例では
Billingがnamespaceとして働きます。InvoiceをBilling::Invoiceという名前で区別できるので、同じInvoiceという名前がほかにあっても衝突しにくくなります。- また、
Billing配下の定数TAX_RATEも同じ箱(=Billingが持つ定数のスコープ)にまとめて管理できます。
つまり、namespaceは同名のクラスや定数を衝突させずに管理する仕組みです。
定数解決の細かい動きは、次回記事のModule.nestingの章で確認します。
(少々お待ちください!)
classとmoduleの違いをコードで確認
classとmoduleの違いをサンプルコードで確認していきましょう。
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点です。
classはnewでインスタンス化できるmoduleはnewできないため、インスタンスを直接作る器ではない
そのため、実務では「オブジェクトを作る本体」はclassに書き、共通機能の再利用や名前整理はmoduleに分けます。
続いて、moduleのインスタンスメソッドとモジュールメソッドの違いを確認します。
moduleのインスタンスメソッドは、includeしたクラスのインスタンスから呼ぶメソッドです。moduleのモジュールメソッドは、AccessRule.method_nameのようにmodule名を付けて直接呼ぶメソッドです。
そして、重要なポイントはmodule本体でdef method_nameとselfなしで書くと、
その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
この結果から次のことを確認できます。
includeでUserへ入るのはinstance_rule(インスタンスメソッド)module_rule(モジュールメソッド)はUserへ入らないため、User.module_ruleは失敗するmodule_ruleはAccessRule.module_ruleのように、moduleをレシーバにして直接呼べる
includeはインスタンスメソッドを混ぜる
moduleの説明の中でも出てきたincludeですが、includeはインスタンスメソッドを混ぜる役割を持っています。
module Greeting def hello "hello" end end class User include Greeting end p User.new.hello # "hello"
Greetingのhelloは、Userから作ったインスタンスで使えるメソッドになります。includeは「このクラスから作るオブジェクト全員に新しい機能を足す」と考えると理解しやすくなります。GreetingはUser.ancestorsに入り、インスタンスメソッド探索経路の一部になります。
ancestorsはclassとmoduleのスーパークラスとインクルードしている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はオブジェクト専用メソッドを追加する
extendはincludeとは違ってオブジェクト専用メソッドを追加する役割を持っています。
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点です。
FinderのメソッドがUserのインスタンスに入ったのではなく、Userというクラスオブジェクト自身に追加される。- そのため
User.new.recentではなくUser.recentでメソッドを呼び出す必要がある
User.new.recentの形式だとインスタンスメソッドの呼び出しなので、きっちり失敗しているのが確認できるかと思います。
また、正確には、FinderはUserのsingleton 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は次のように使い分けます。
- 既存メソッドの前後で処理を挟みたい
- インスタンスメソッドを差し替えたいなら
classへprepend - クラスメソッドを差し替えたいなら
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.
わかりやすくすると以下の流れになります。
prependされたモジュール(新しいものが先頭)- クラス自身
includeされたモジュール(新しいものが先頭)- スーパークラス側へ順に移動
- 同様のルールで再帰
ですが、これを目視で追っていくのは大変ですよね?
そんなときに便利なメソッドが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で見る
インスタンスメソッドだけでなく、クラスメソッドにおいても「どの順番で探すか」を確認できます。
ここではextendとprependをクラスオブジェクト側に入れた例を見ます。
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手順と出力の対応は次のとおりです。
- ①:
Adminと[Admin]が、class Admin本体の実行中であることを示す - ②:
Admin::UserPolicyと[Admin::UserPolicy, Admin]が、内側クラス実行中であることを示す - ③:
Adminが内側クラスを抜けて外側へ戻ったことを示す - ④:
[:UserPolicy]とAdmin::UserPolicyが、UserPolicyがAdminの定数として登録済みであることを示す
出力結果からわかる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.nestingにAdminが入っていません。
これは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点です。
module Billingに入ると、その時点のselfはBillingになるmodule Taxに入ると、selfはBilling::Taxに切り替わるmodule Taxを抜けると、selfはBillingへ戻る- 内側で定義した
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.nestingにBillingが入りません。
そのため、クラスのときと同様に定数の動作に違いが出てきます。
まとめ
今回の要点は次の5つです。
- 継承は「親を探索順へ追加する仕組み」で、
superは「次に見つかる実装」を呼ぶ include/extend/prependは、moduleを差し込む場所がそれぞれ違う- インスタンスメソッドの呼び出し先は
ancestorsで確認できる - クラスメソッドの呼び出し先は
singleton_class.ancestorsで確認できる - ネスト定義と定数パス定義では
Module.nestingの結果が変わり、定数解決の挙動も変わる
次回(第3回)は、今回触れたModule.nestingの続きとして、定数スコープと可視性(public/private/protected)を整理します。
参考資料
- Ruby公式:
modules_and_classes(クラス定義・継承・ネスト) - Ruby公式:
calling_methods(superとメソッド呼び出し規則) - Ruby公式:
calling_methods(Method Lookup) - Ruby公式:
Module(include/prepend/ancestors/nesting) - Ruby公式:
Class - Ruby公式:
Object(extend)
テコテックの採用活動について
テコテックでは新卒・中途採用を積極的に行っています。 採用サイトでは会社の雰囲気や福利厚生、募集ポジションをご確認いただけます。 ご興味をお持ちいただけましたら、ぜひチェックしてみてください。 tecotec.co.jp