prepend
は、既にあるコードに手を加えずに、処理をラップするのに便利です。
例えば、下記のように書けば、Aクラスは書き換えずに、hogehogeメソッドをラップした処理を書くことができます。
class A
def hogehoge
puts "A#hogehoge"
end
end
module PrependModule
def hogehoge
puts "hogehoge is called!!"
super
end
end
A.prepend PrependModule
A.new.hogehoge
hogehoge is called!!
A#hogehoge
上記は単純な例ですが、オブジェクトの生成タイミング、継承、include(mix-in)によって、prependはどのように動作を変えるのでしょうか?
親クラスに対してprependし、呼び出すのは子クラスのオブジェクトの場合
class ParentClass
def hogehoge
puts "ParentClass#hogehoge"
end
end
class ChildClass < ParentClass
end
module PrependModule
def hogehoge
puts "PrependModule#hogehoge"
super
end
end
p ParentClass.ancestors
p ChildClass.ancestors
ParentClass.prepend PrependModule
# prependされているのが親クラスのメソッドで、レシーバも親は当然有効
ParentClass.new.hogehoge
# prependされているのが親クラスのメソッドで、レシーバが子でも有効
ChildClass.new.hogehoge
p ParentClass.ancestors
p ChildClass.ancestors
[ParentClass, Object, Kernel, BasicObject]
[ChildClass, ParentClass, Object, Kernel, BasicObject]
PrependModule#hogehoge
ParentClass#hogehoge
PrependModule#hogehoge
ParentClass#hogehoge
[PrependModule, ParentClass, Object, Kernel, BasicObject]
[ChildClass, PrependModule, ParentClass, Object, Kernel, BasicObject]
親クラスに対してprependした場合でも、子クラスのオブジェクトのメソッド呼び出しはPrependModule#hogehoge
を通過します。
このことはprepend後の継承ツリーを見てもわかります。子クラス、親クラスともに継承ツリーにはPrependModule
が差し込まれていますね。
オブジェクト生成後にprependがされた場合
class TargetClass
def hogehoge
puts "TargetClass#hogehoge"
end
end
module PrependModule
def hogehoge
puts "PrependModule#hogehoge"
super
end
end
p TargetClass.ancestors
parent = TargetClass.new
TargetClass.prepend PrependModule
parent.hogehoge
p TargetClass.ancestors
[TargetClass, Object, Kernel, BasicObject]
PrependModule#hogehoge
TargetClass#hogehoge
[PrependModule, TargetClass, Object, Kernel, BasicObject]
オブジェクト生成後にprependが行われた場合でも、prepend前に生成したオブジェクトのメソッド呼び出し時にPrependModule#hogehoge
を通過します。
継承ツリーを見れば、なるほど、納得感があります。
includeするModuleに対してprependするが、prepend前にincludeが行われる場合
module IncludeModule
def hogehoge
puts "IncludeModule#hogehoge"
end
end
class TargetClass
include IncludeModule
end
module PrependModule
def hogehoge
puts "PrependModule#hogehoge"
super
end
end
p TargetClass.ancestors
p IncludeModule.ancestors
IncludeModule.prepend PrependModule
target = TargetClass.new
target.hogehoge
p TargetClass.ancestors
p IncludeModule.ancestors
[TargetClass, IncludeModule, Object, Kernel, BasicObject]
[IncludeModule]
IncludeModule#hogehoge
[TargetClass, IncludeModule, Object, Kernel, BasicObject]
[PrependModule, IncludeModule]
PrependModule#hogehoge
は反応しませんでした。
prepend前後の継承ツリーを見てみると、includeはあくまでinclude時点での情報をmix-inするに過ぎないようです。
そのため、後からIncludeModule
に対してprependしたところで、既にmix-inが完了しているTargetClass
の継承ツリーに影響を与えることはなかったのだと思います。
includeするModuleに対してprependするが、prepend後にincludeが行われる場合
前項で記述したことを正しいか確認するために、今度はprepend後にmix-inさせてみます。
module IncludeModule
def hogehoge
puts "IncludeModule#hogehoge"
end
end
class TargetClass
end
module PrependModule
def hogehoge
puts "PrependModule#hogehoge"
super
end
end
p TargetClass.ancestors
p IncludeModule.ancestors
IncludeModule.prepend PrependModule
TargetClass.class_eval { include IncludeModule }
target = TargetClass.new
target.hogehoge
p TargetClass.ancestors
p IncludeModule.ancestors
[TargetClass, Object, Kernel, BasicObject]
[IncludeModule]
PrependModule#hogehoge
IncludeModule#hogehoge
[TargetClass, PrependModule, IncludeModule, Object, Kernel, BasicObject]
[PrependModule, IncludeModule]
想定通りです。TargetClass
には、PrependModule
がprependされた状態のIncludeModule
がincludeされるので、継承ツリーにもPrependModule
が差し込まれることになります。
まとめ
Classに対してprependを差し込む場合、今回の例ではオブジェクトの生成タイミングや親子関係を意識する必要がないことがわかりました。
が、Moduleのインクルードについては、includeした時点でのModuleの継承ツリー情報が差し込まれることがわかりました、
そのため、prependで何か処理をラップしたい場合、Moduleがincludeされるタイミングを意識しなければならないことがわかりました。
通常は意識することがないかもしれませんが、もしかすると、prependをかけるタイミングをうまく制御しないとダメな局面があるかもしれません。
追記(2020.03.17)
こちらの記事の方が背景・検証結果ともにわかりやすい、安心できるソースなので是非参照ください。