LoginSignup
4
3

More than 3 years have passed since last update.

様々なシチュエーションでprependをした場合の振る舞いについて調べてみた

Last updated at Posted at 2017-02-17

prependは、既にあるコードに手を加えずに、処理をラップするのに便利です。
例えば、下記のように書けば、Aクラスは書き換えずに、hogehogeメソッドをラップした処理を書くことができます。

prepend_sample.rb
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し、呼び出すのは子クラスのオブジェクトの場合

prepend_inheritance.rb
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がされた場合

prepend_after_new.rb
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が行われる場合

prepend_include.rb
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させてみます。

prepend_include_lazy.rb
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)

こちらの記事の方が背景・検証結果ともにわかりやすい、安心できるソースなので是非参照ください。

Ruby: includeしたモジュールに「後から」別のモジュールを動的にincludeしても反映されない

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3