Rubyで親クラスから子クラスの定数を参照

スポンサーリンク

Ruby で親子関係の継承クラスで、親クラス(スーパークラス)から子クラス(サブクラス)の定数を参照したい機会があった。親クラスに共通化して定義したいメソッドの中で、子クラスで定義されている定数を参照したいケースです。

【お知らせ】 英単語を画像イメージで楽に暗記できる辞書サイトを作りました。英語学習中の方は、ぜひご利用ください!
画像付き英語辞書 Imagict | 英単語をイメージで暗記
【開発記録】
英単語を画像イメージで暗記できる英語辞書サービスを作って公開しました
スポンサーリンク

— 環境 —
Ruby 2.2.2
Rails 4.2

※ 2015/11/12 記事の末尾に追記しました。

リファクタリング前のコード

単純化してますけど、具体的には以下のようなサンプルで、最初に書いたのが次のようなコード。

子クラス(Male, Female)に、その人が結婚できる年齡かどうかを判定する marriageable? メソッドを定義しています。

このコードはちゃんと動きますけど、子クラスの marriageable? メソッドが重複しているので、DRY 違反をなくすために親クラス(Person クラス)に marriageable? メソッドを移動させたかった。

そこで、以下のように単純にそのまま子クラスの marriageable? メソッドを削除して、親クラス(Person クラス)に移動させますと…

今度は、「NameError: uninitialized constant Person::MARRIAGE_AGE」が発生します。定数 MARRIAGE_AGE は Person クラスに定義されておらず、marriageable? メソッドは Person クラスの MARRIAGE_AGE 定数を参照するため。

リファクタリング後のコード

ということで、子クラスで定義された定数(MARRIAGE_AGE)を、親クラス(Person クラス)内から参照するには「self.class::MARRIAGE_AGE」と書く必要がある。

marriageable? メソッドを親クラス(Personクラス)に移動させて、子クラスで定義されている定数 MARRIAGE_AGE を参照するように、self.class::MARRIAGE_AGE と変更しました。これですっきりすると共に、子クラス(Male, Female)のインスタンスから marriageable? メソッドを正しく呼び出せるようになりました。「pp self.class」の行は、インスタンスのクラスを確認するデバッグ用のためだけ。

ActiveRecord の例を確認してみた

ちょっとこの書き方で良いのか確信がなかったので、Rails の ActiveRecord で使用例がないか「self.class::」で grep をかけてみたところ、同様の使い方が見つかりました。該当の箇所だけ抜粋。

lib/active_record/connection_adapters/abstract_adapter.rb

lib/active_record/connection_adapters/abstract_mysql_adapter.rb

lib/active_record/connection_adapters/mysql2_adapter.rb

lib/active_record/connection_adapters/mysql_adapter.rb

lib/active_record/connection_adapters/postgresql_adapter.rb

lib/active_record/connection_adapters/sqlite3_adapter.rb

Mysql2Adapter, MysqlAdapter, PostgreSQLAdapter, SQLite3Adapter など DB アダプタークラスのインスタンスから、親クラス(AbstractAdapter クラス)に定義されている adapter_name メソッドを呼び出るようになっています。ということで、Rails でも使ってあるのでこの書き方で良さそう。

最後ちょっと本題とは関係ないのですけど、ActiveRecord のソースコードを読んでいて文字列リテラルの定数に freeze が付きまくってるのを見て、以下の記事を思い出した。

[Ruby] Ruby 3.0 の特大の非互換について – まめめも

【追記 2015/11/12】
haruharu1 さんにはてなブックマークでコメントを頂きました。ありがとうございました。

haruharu1
Personクラスに『定数=nil』と、marriageable?かけばいいんでね。

これ最初自分も同じように考えて試してみたのですが、上手く動作しませんでした。文面が長くなりすぎると考えて割愛したのですけれど。おそらく以下のようなコードを想定されているかと思います。

上記コードの動作を確認しますと、やはり Person#marriageable? メソッド中の MARRIAGE_AGE は、Person::MARRIAGE_AGE を参照しに行きます。その結果、@age (Fixnum) と nil の比較になってエラー発生。

nil じゃなくて数値として Person::MARRIAGE_AGE を 0 歳にしてみますと…

となり、やはり Person::MARRIAGE_AGE を参照して結婚可能な年齡が 0 歳になるので、17歳の男性でも0歳児でも結婚できる世界になってしまう。Ruby の定数をインスタンスメソッドから参照する場合は、常にそのクラス定義のスコープ中の定数を呼び出す挙動となるようです。

なので、自分の結論としては、親クラスから子クラスの定数を参照したい場合は、「self.class::MARRIAGE_AGE」と、インスタンスのクラスを明示して書く必要があるかと。上述した ActiveRecord のアダプター関連クラスでも、親クラス(AbstractAdapter クラス)で「ADAPTER_NAME = ‘Abstract’.freeze」と定義しつつ、子クラスの定数参照には「self.class::ADAPTER_NAME」と書いてありましたので。もしかして間違っている場合はご指摘頂けると助かります!
【追記ここまで】

スポンサーリンク
私は以下の本で Ruby を覚えました。メタプログラミングRubyは入門を超える内容で難しめです。
 
スポンサーリンク

Leave Your Message!