Refactormycodeを見てたら、method_missingを使った例があったので、試しに使ってみた。
method_missingとfind_by_xxx
Railsには、このmethod_missingを使ったハックがある。find_by_name、find_all_by_nameとかのfind_xxxメソッドだ。
method_missingは通常はそのメソッドがなかった時にエラーを発生させる仕事をしている。(リファレンス)Railsでは、このメソッドをオーバーライドして、find_by_xxxを実現している。
find_by_xxxの仕組み
例えば、Userモデルがあり、usersテーブルにnameカラムがあるとする。この時、find_by_nameは簡単に言えば次のよう処理を行う。
1.User.find_by_name("bob")を呼ぶ
2.Uesrクラスにfind_by_nameメソッドは定義されていないので、method_missingが呼ばれる。
3.method_missingの中でfind_by_nameのbyとnameを切り出す。
4.byを切り出したので、呼び出すメソッドはfind(:first)となる。(find_all_byならfind(:all)を呼ぶ)
5.nameを切り出したので、findメソッドの引数に、conditions => ["name => ?","bob"]が渡される。
この例では、メソッドの名前から、検索対象になるカラムを抽出している。
自分でもやってみた
考えたのはこんな感じ。(注:最後に書いたが、この例は良くない)
- Userクラスと、そのプロフィールを保持するクラスがある
- Userには、学生と社会人がいて、それぞれ別のプロフィールを持つ
- 学生のプロフィールをStudentProfile、社会人のプロフィールをWorkerProfileとする
プロフィールの親クラスとしてAbstractProfileを定義する(この例では不要だった)
ここまでを実装してみる
- User
class User attr_reader :name,:age,:type,:profile def initialize(args) @name = args[:name] @age = args[:age] @type = args[:type] @profile = args[:profile] end end
AbstractProfile(Workerクラスにないはずのschoolが親クラスにあるのは変)
class AbstractProfile def school; nil; end def company; nil; end end
- WorkerProfile
class WorkerProfile attr_reader :company def initialize(company) @company = company end end
- StudentProfile
class StudentProfile attr_reader :school def initialize(school) @school = school end end
このままだと、Userの学校名を知りたい場合に、@user.profile.schoolのようにprofileを挟んで呼び出すことになる。プロフィールは別クラスに持っておきたいが、@user.schoolと呼び出したい。
ここでmethod_missingを使うことができる。(いわゆる委譲を実装する)
method_missingを導入
Userクラスを次のように変更する。
class User attr_reader :name,:age,:type,:profile def initialize(args) @name = args[:name] @age = args[:age] @type = args[:type] @profile = args[:profile] end private def method_missing(method_id,*args) if profile.respond_to?(method_id) profile.send(method_id) else super end end end
インスタンスメソッドに対するNoMethodErrorを補足するので、上のように書く。クラスメソッドに対するエラーを補足したい場合は、method_missingをクラスメソッドとして定義する。
これで、@user.schoolのように書けるようになる。
テスト
- UserTest
require 'test/unit' require 'user' require 'worker_profile' require 'student_profile' class UserTest < Test::Unit::TestCase def setup @bob = User.new(:name => "bobby", :age => 27, :type => "worker", :profile => WorkerProfile.new("Abc company")) @becky = User.new(:name => "rebecka", :age => 17, :type => "student", :profile => StudentProfile.new("Xyz school")) end def test_user #bob assert_equal "bobby", @bob.name assert_equal 27, @bob.age assert_equal "worker", @bob.type assert_equal "Abc company", @bob.profile.company #becky assert_equal "rebecka", @becky.name assert_equal 17, @becky.age assert_equal "student", @becky.type assert_equal "Xyz school", @becky.profile.school end def test_method_missing assert_equal "Abc company", @bob.company assert_equal "Xyz school", @becky.school assert_raise(NoMethodError){@bob.hoge} end end
rubikitchさんにトラバで突っ込まれたよ
委譲といえばForwardable-’(rubikitch wanna be (a . lisper))
method_missingは多くの場合デバッグが困難になるので乱用厳禁。あくまで最終手段にすべき。DRbのように相手が得体の知れないオブジェクトならばmethod_missingは有効だが、よくわかってるオブジェクト相手には基本的にmethod_missingは使用すべきではない。
Userクラスを読んだときに、取り得るメソッドの集合が一目でわからないところが気持ち悪い。
う、やっぱりつっこまれた。悪例でした。すみませんm(_ _)mトラバ先には、代わりに以下の方法が書いてあった。
- define_methodを使う
これは知ってた。Refactormycodeにも、define_method使う例があったから。
- Forwardableを使う
これは知らなかった(・∀・)使うとこう書けるようになる。
extend Forwardable def_delegators :@profile, :school, :company
なるほど。確かに、Userクラスのメソッドが明示的に分かる。
委譲としてのmethod_missingはよくなかったな。勉強になった☆ribikitchさんトラバありがとうございます!