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

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

```ruby: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し、呼び出すのは子クラスのオブジェクトの場合
```ruby: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がされた場合
```ruby: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が行われる場合
```ruby: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させてみます。

```ruby: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しても反映されない](https://techracho.bpsinc.jp/hachi8833/2019_01_23/66108)