- 更新日: 2015年11月12日
- Ruby
Rubyで親クラスから子クラスの定数を参照
Ruby で親子関係の継承クラスで、親クラス(スーパークラス)から子クラス(サブクラス)の定数を参照したい機会があった。親クラスに共通化して定義したいメソッドの中で、子クラスで定義されている定数を参照したいケースです。
— 環境 —
Ruby 2.2.2
Rails 4.2
※ 2015/11/12 記事の末尾に追記しました。
リファクタリング前のコード
単純化してますけど、具体的には以下のようなサンプルで、最初に書いたのが次のようなコード。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class Person def initialize(name, age) @name = name @age = age end def get_old @age += 1 end end class Female < Person MARRIAGE_AGE = 16 def marriageable? @age >= MARRIAGE_AGE end end class Male < Person MARRIAGE_AGE = 18 def marriageable? @age >= MARRIAGE_AGE end end taro = Male.new('Taro', 17) taro.marriageable? # => false taro.get_old # => 18 taro.marriageable? # => true |
子クラス(Male, Female)に、その人が結婚できる年齡かどうかを判定する marriageable? メソッドを定義しています。
このコードはちゃんと動きますけど、子クラスの marriageable? メソッドが重複しているので、DRY 違反をなくすために親クラス(Person クラス)に marriageable? メソッドを移動させたかった。
そこで、以下のように単純にそのまま子クラスの marriageable? メソッドを削除して、親クラス(Person クラス)に移動させますと…
1 2 3 4 5 6 |
class Person def marriageable? @age >= MARRIAGE_AGE end # ... |
今度は、「NameError: uninitialized constant Person::MARRIAGE_AGE」が発生します。定数 MARRIAGE_AGE は Person クラスに定義されておらず、marriageable? メソッドは Person クラスの MARRIAGE_AGE 定数を参照するため。
リファクタリング後のコード
ということで、子クラスで定義された定数(MARRIAGE_AGE)を、親クラス(Person クラス)内から参照するには「self.class::MARRIAGE_AGE」と書く必要がある。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class Person def initialize(name, age) @name = name @age = age end def marriageable? pp self.class @age >= self.class::MARRIAGE_AGE end def get_old @age += 1 end end class Female < Person MARRIAGE_AGE = 16 end class Male < Person MARRIAGE_AGE = 18 end taro = Male.new('Taro', 17) taro.marriageable? # Male => false taro.get_old # => 18 taro.marriageable? # Male => true |
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
1 2 3 4 5 6 7 |
module ActiveRecord module ConnectionAdapters # :nodoc: class AbstractAdapter ADAPTER_NAME = 'Abstract'.freeze def adapter_name self.class::ADAPTER_NAME end |
lib/active_record/connection_adapters/abstract_mysql_adapter.rb
1 2 3 |
module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter |
lib/active_record/connection_adapters/mysql2_adapter.rb
1 2 3 4 |
module ActiveRecord module ConnectionAdapters class Mysql2Adapter < AbstractMysqlAdapter ADAPTER_NAME = 'Mysql2'.freeze |
lib/active_record/connection_adapters/mysql_adapter.rb
1 2 3 4 |
module ActiveRecord module ConnectionAdapters class MysqlAdapter < AbstractMysqlAdapter ADAPTER_NAME = 'MySQL'.freeze |
lib/active_record/connection_adapters/postgresql_adapter.rb
1 2 3 4 |
module ActiveRecord module ConnectionAdapters class PostgreSQLAdapter < AbstractAdapter ADAPTER_NAME = 'PostgreSQL'.freeze |
lib/active_record/connection_adapters/sqlite3_adapter.rb
1 2 3 4 |
module ActiveRecord module ConnectionAdapters class SQLite3Adapter < AbstractAdapter ADAPTER_NAME = 'SQLite'.freeze |
Mysql2Adapter, MysqlAdapter, PostgreSQLAdapter, SQLite3Adapter など DB アダプタークラスのインスタンスから、親クラス(AbstractAdapter クラス)に定義されている adapter_name メソッドを呼び出るようになっています。ということで、Rails でも使ってあるのでこの書き方で良さそう。
最後ちょっと本題とは関係ないのですけど、ActiveRecord のソースコードを読んでいて文字列リテラルの定数に freeze が付きまくってるのを見て、以下の記事を思い出した。
[Ruby] Ruby 3.0 の特大の非互換について – まめめも
【追記 2015/11/12】
haruharu1 さんにはてなブックマークでコメントを頂きました。ありがとうございました。
haruharu1
Personクラスに『定数=nil』と、marriageable?かけばいいんでね。
これ最初自分も同じように考えて試してみたのですが、上手く動作しませんでした。文面が長くなりすぎると考えて割愛したのですけれど。おそらく以下のようなコードを想定されているかと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class Person MARRIAGE_AGE = nil def initialize(name, age) @name = name @age = age end def marriageable? pp MARRIAGE_AGE @age >= MARRIAGE_AGE end def get_old @age += 1 end end class Female < Person MARRIAGE_AGE = 16 end class Male < Person MARRIAGE_AGE = 18 end taro = Male.new('Taro', 17) taro.marriageable? # nil # => ArgumentError: comparison of Fixnum with nil failed |
上記コードの動作を確認しますと、やはり Person#marriageable? メソッド中の MARRIAGE_AGE は、Person::MARRIAGE_AGE を参照しに行きます。その結果、@age (Fixnum) と nil の比較になってエラー発生。
nil じゃなくて数値として Person::MARRIAGE_AGE を 0 歳にしてみますと…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class Person MARRIAGE_AGE = 0 def initialize(name, age) @name = name @age = age end def marriageable? pp MARRIAGE_AGE @age >= MARRIAGE_AGE end def get_old @age += 1 end end class Female < Person MARRIAGE_AGE = 16 end class Male < Person MARRIAGE_AGE = 18 end taro = Male.new('Taro', 17) taro.marriageable? # 0 => true jiro = Male.new('Jiro', 0) jiro.marriageable? # 0 => true |
となり、やはり Person::MARRIAGE_AGE を参照して結婚可能な年齡が 0 歳になるので、17歳の男性でも0歳児でも結婚できる世界になってしまう。Ruby の定数をインスタンスメソッドから参照する場合は、常にそのクラス定義のスコープ中の定数を呼び出す挙動となるようです。
なので、自分の結論としては、親クラスから子クラスの定数を参照したい場合は、「self.class::MARRIAGE_AGE」と、インスタンスのクラスを明示して書く必要があるかと。上述した ActiveRecord のアダプター関連クラスでも、親クラス(AbstractAdapter クラス)で「ADAPTER_NAME = ‘Abstract’.freeze」と定義しつつ、子クラスの定数参照には「self.class::ADAPTER_NAME」と書いてありましたので。もしかして間違っている場合はご指摘頂けると助かります!
【追記ここまで】
- Ruby の関連記事
- Gemの作り方(Ruby Gem)
- ローカル開発中のgemをGemfileに書いてインストール
- 熊本地震の余震が夜に多いのは本当か?Rubyプログラムで検証してみた
- El Capitanでgemのnative extensionビルド失敗に対応
- MacabをRubyで使う
- rbenv/ruby-buildでRuby最新バージョンをインストール
- Rubyでクラスインスタンス変数にインスタンスメソッドからアクセス
- 距離1kmあたりの緯度・経度の度数を計算(日本・北緯35度)
- Google Maps Geocoding APIで住所から緯度・経度を取得するRubyコード
- Yahoo地図API(YOLP)のジオコーダAPIで住所から緯度・経度を求めるRubyコード
- 初回公開日: 2015年11月11日
Leave Your Message!