トップ画像イメージ

呼び出すメソッドを動的に変更する(取り扱い要注意!)

こんにちは。 今回、筆者の仕事でRubyのsendメソッドとevalメソッドを利用して、動的に実行するメソッドを変更するという、メタプログラミングっぽいことをやってみましたので、記載していこうと思います。

今回の記事のポイント

  • Rails(Ruby)で実行するメソッドを動的に変更したい
  • 文字列からその文字列と同じ名前を持つメソッドを実行したい
    • (例)文字列"all"からUser.allを実行したい
  • 文字列からその文字列と同じ名前を持つ変数を取得したい
    • (例)文字列'users'からusers = User.allという風に定義している変数(オブジェクト)usersを取得したい

if文やcase文との違い

動的に変更するって言うと、if文と何が違うのか?

条件によって、動作を変更させるといって、すぐに思いつくのは、if文やcase文だと思います。

例えば、以下のような形になります。


user = User.first
str = "name"

if str == "name"
  puts user.name
elsif str == "id"
  puts user.id
end

↑では、あらかじめ決まったメソッドを、決められた条件の時に呼び出しています

上の例では、strが"name"の時には、nameメソッドを、strが"id"の時には、idメソッドを実行して欲しいという風にしています。

御察しの通り、nameだのidだのは、テーブルの列名です。 今くらいの列名の条件分岐ならば、これで良いと思いますが、例えばUserモデルが列を50持っていて、全てに対応する条件分岐を書かなければいけないなら、どうなるでしょうか?

user = User.first
str = "name"

case str
when "name"
  puts user.name
when "id"
  puts user.id
when "kana_name"
  puts user.kana_name
when
#「後46個だ!頑張ろう!」・・・とは思えない・・・

「テーブル定義の仕様変更で列名の増減・変更があるよ!!メンゴ!!」とか言われた日には、死んでしまいます😢 (アジャイルではよくあること)

ということで、上のように、あらかじめプログラマーが決めている条件で、メソッドを変えるのではなく

strが"name"なら、nameメソッドを実行したいし、"id"ならidメソッドを実行したいの!数行で終わらせたい!いちいち一つ一つの条件を書かなくても、そのくらい判断してよ!

ということが、「動的にメソッドを実行したい」ということかと思います。

メタプログラムっていうのは、プログラムを作る、実行するプログラムっていうことらしいですよ。今回なら、プログラムを動かすプログラムを書いておくってことになるかな?

sendメソッドを使う

上のように、strが"name"の時にはnameメソッドを、strが"id"の時にはidメソッドを、実行させたいという時には、下のようにプログラムをかけます。

user = User.first
str = "name"

puts user.send(str)
#user.nameが実行され次のように出力される
=>"imanau"
これだけで出来んのか!!

TIP

この#sendメソッドは、 レシーバーが持つメソッドを実行してくれます。メソッドを呼び出す時には、引数も渡すことができます。

このように、記述することで、実行するメソッドを動的に変更することができます。

ただ・・・

話はこれだけでは終わりません。もっと動的に条件を実行してみましょう。

evalメソッドも使ってみよう(取り扱い要注意)

今までの例では、動的に変更していく部分は、どのメソッドを使うかという点だけでした。

「どのメソッド」をだけでなく「何に」メソッドを使用するかも動的に制御してね!って上司から言われた・・・

要するにこういうことになります。

#その時、その時によって与えられるモデル名とメソッドが違う
model = "User" #UserだったりContentだったり
mes = "id" #idだったり、nameだったり

#↑の場合だとUser.first.idを実行する必要がある。

こういう時には、いくつか方法があると思いますが、evalメソッドという方法が利用できます。

実際に使ってみると、下のように書くことができます。

######
#その時、その時によって条件が変わるゾーン
model = "User" 
mes = "name"
######

object = eval(model)
#object = Userが実行される

object.first.send(mes)
=>"imanau"

今回使った、evalメソッドは、

引数として与えられた文字列をRubyのプログラムとして実行してくれるメソッドです。

下のように、利用することができます。

eval("puts imanau")
#puts imanauが実行される
=>imanau

※ちなみに、Railsであれば、"User".constantizeというメソッドを使えば、モデル(クラス)が取得できるため、evalを使わなくても同じことはできます。

今回は、'User'という文字列をevalを使ってRubyのコードに変換した結果、Userというモデルを取得したわけですね。

・・・ですが、御察しの通りevalはとても危険なメソッドでもあります。

だって、文字列をそのままコードとして実行できちゃうんですよ。

つまり、悪い奴がdelete_allとか指定してきちゃうと、全ての情報を消されてしまいます。

なので間違っても、下のような使い方をしてはいけません。

str = params[:str]
eval(str)
#絶対だめ!!ユーザーから送られる文字列をそのまま、evalに流すなんて危険すぎます。

DANGER

evalメソッドは、強力であるがゆえに、非常に危険。もちろん、sendでも同じことが言えますので、厳重に注意してください!!ユーザーの入力値をそのまま使うなんて、絶対にやってはいけません。

例えば、あるメソッドの返り値次第(例えばテーブルから値を取り出してきて、返却するメソッド等)で、条件を動的に変えたい時など、悪意を持ったユーザーに利用されないよう注意しましょう。(というか、必要がなければ使わなくて良い)

まとめ

  • メタプログラミングは非常に強力であるが、非常に危険
    • sendやevalなど、取り扱いには非常に注意が必要
  • ただ、使いこなせば多分相当便利です