6章 金曜日: コードを記述するコード
金曜日のアジェンダ
- クラスマクロとの恋
- evalを使おうよ!
- eval使うのやめようよ!
- クラスマクロにしようよ!
- フックメソッドを使おうよ!
1. クラスマクロとの恋
-
そもそもクラスマクロって?
クラス定義内でselfを明示せずに呼び出せるキーワードのようなメソッド ex) attr_accessor
クラスマクロとの邂逅
亀ちゃん「クラスマクロってメソッドだったの!? すげえ、俺も独自のクラスマクロ作りてえ!」
亀ちゃんは最近物忘れがひどく、自分がいつインスタンス変数を作ったのか忘れてしまう。そのため、インスタンス変数をハッシュにして、時間のデータを保存できる様にするクラスマクロを実装したい様だ。亀ちゃんの希望
Class Intern
attr_with_date :shimatomo
attr_with_date :kemako
end
intern = Intern.new
intern.shimatomo = 'shimada'
intern.kemako = 'miyatake'
intern.shimatomo[:val] # => 'shimada'
intern.shimatomo[:created_at] # => 3行上で作成した時のTime
2. evalを使おうよ!
よしけん「それじゃあまず、attr_accessorをただのメソッドとして実装してみようか。ちょうどいいから、ここでevalを使ってみよう。」
亀ちゃん「」
けまこ「evalは、引数として渡した文字列をコードとして実行します。詳しくはメタプロ参照。」
Q1. add_attr_with_dateメソッドの挙動は以下の様になるように実装
class Intern; end
add_attr(Intern, :shimatomo)
intern = Intern.new
intern.shimatomo = 'shimada'
puts intern.shimatomo # 'shimada'
def add_attr_with_date(klass, attr)
eval "
#code...
"
end
# ヒント!
# hogeと言う名前のattrを受け取った場合に以下の様なメソッドを定義してほしい。
def hoge=(value)
@hoge = value
end
def hoge
@hoge
end
A1. add_attr
def add_attr_with_date(klass, attribute)
eval "
class #{klass}
def #{attribute}
@#{attribute}
end
def #{attribute}=(val)
@#{attribute} = valf
end
end
"
end
# 亀ちゃんさんの希望では次の様になる
def #{attr}=(val)
@#{attr} = {val: val, created_at: Time.now}
end
亀ちゃん「evalを使えば、いつもは動的に設定できないクラス名もメソッド名も、インスタンス変数名も動的に定義できるなんて!」
よしけん「だけど思い出して、大いなる力には、、?」
亀ちゃん & 馬場っち「大いなる責任が伴う!!」
けまこ「evalに渡される文字列がinputなど第三者の入力を含むとき、そのコードに悪意がある場合でもそのまま実行されてしまいます。基本的にevalを使う際には、渡されるコード文字列全てが自分の入力であることを確かめましょうね。」
3. evalを使うのやめようよ!
亀ちゃん「う〜ん、evalのリスクを完全に取り除くのは難しそうだなぁ。。」
よしけん「亀ちゃん、カレントクラスを動的に変更する方法はeval以外にもあったよね?」
亀ちゃん「あっ、水曜日にやったやつだ!」
けまこ「と言うことで、class_evalを使ってadd_attr_with_dateを変更しましょう」
A2.
def add_attr_with_date(klass, attribute)
klass.class_eval do
define_method "#{attribute}=" do |value|
attr_hash = { val: value, created_at: Time.new }
instance_variable_set("@#{attribute}", attr_hash)
end
define_method attribute.to_s do
instance_variable_get("@#{attribute}")
end
end
end
4. クラスマクロにしようよ!
亀ちゃん「でもこんなのクラスマクロじゃない!!」
よしけん「わかったわかった笑 次はようやくクラスマクロに行こうね。」
亀ちゃん「😲 」
けまこ「ようやくクラスマクロを作るときがきました。亀ちゃんの目が爛々と輝きを増しています。」
Q3. 以下の変更点を踏まえてメソッドをクラスマクロにしよう。下のコードは期待される結果です。
- クラスを明示的に引数に渡さなくする
- メソッドを定義する場所を変える(どのクラスからでも呼び出せる様に)
class Intern
attr_with_date :shimatomo
end
intern= Intern.new
intern.shimatomo = 'shimada'
intern.shimatomo[:val] # => 'shimada'
intern.shimatomo[:created_at] # => Timeオブジェクト
A3
class Module # もしくはClassクラス
def add_attr_with_date(attr)
define_method(attr) do
instance_variable_get("@#{attr}")
end
define_method("#{attr}=")do |val|
instance_variable_set("@#{attr}", {
val: val,
created_at: Time.now
})
end
end
end
亀ちゃん「できた!!!」
よしけん「そう、ClassクラスかModuleクラスにメソッドを定義するとクラスマクロとして使えるんだね。なんでかは分かる?」
中山さん「木曜日のクラス定義でやったところですね。Rubyのオブジェクトモデルにおけるメソッドの探索の挙動は、メソッドを呼び出したレシーバの特異クラスを探索し、次にその特異クラスのsuperclassを探索し、そしてまた次のsuperclassを探索、、、と言う様に続きます。その際に注意すべきなのが、BasicObjectの特異クラスのsuperclassはClassクラスだということです。木曜日にやりましたが。つまり全てのクラス(Classクラスのインスタンス)はメソッド探索の過程でClassクラスとModuleクラスを必ず通る様になっていて、その結果として全クラス内でselfを明示することなく、キーワードの様にClassクラスとModuleクラスのインスタンスメソッドを呼び出せます。これがクラスマクロの正体ですね。ClassクラスとModuleクラスの次がObjectクラス、BasicObjectクラスが最後、という風に続きますが、これは全クラスだけでなくクラスのインスタンスオブジェクトを含む全てのオブジェクトも探索しにきてしまうためクラスマクロとしてはhogehoge.....」
のりピー「やんなぁ」
5. フックメソッドを使おうよ!
よしけん「クラスマクロを完成させたのはいいけど、このままだと全てのクラスから呼び出せてしまうよね。」
亀ちゃん「そんなのやだ!限られた相手と合意の上(includeされた上)だけにしてほしい!」
かっちゃん「亀ちゃんくんは少々束縛癖をこじらせているみたいだね。」
けまこ「そんな亀ちゃんのために、オブジェクトモデルでの様々な変化(継承、include、extend)などのイベントがあった際に特定の処理を設定できるフックメソッドがあります。これでいろんな制限や特殊な処理を設けられますね。最低限self.includedの使い方だけは理解して次の問題に臨みましょうね。」
Q4. クラス内でincludeした時にクラスメソッドが使える様になるMyModuleを作成
Class Intern
include MyModule
add_attr_with_date :shimatomo
end
obj = Intern.new
obj.shimatomo = 'shimada'
obj.shimatomo[:val] # => 'shimada'
obj.shimatomo[:created_at] # => Timeオブジェクト
A4.
module MyModule
def self.included(klass)
class << klass
def add_attr_with_date(attr_name)
self.class_eval do
define_method "#{attr_name}=" do |value|
instance_variable_set("@#{attr_name}", { val: value, created_at: Time.now })
end
define_method "#{attr_name}" do
instance_variable_get("@#{attr_name}")
end
end
end
end
end
end
亀ちゃん「self.included
メソッドの中で単純にメソッド定義するだけだとインスタンスメソッドとしてしか使えないけど、class << klass
部分でklassの特異クラスをオープンしメソッド定義することで、クラスマクロを実現したんだ!」
よしけん「このフックメソッドの使い方は、実際にサーベイのコードとかにも出てくるから、覚えておくといいね。」
けまこ「これで金曜日も終わり、長かったメタプロの週に終わりが来ました。これであなたもRubyistやね!」