RubyのInheritanceとMixinでのメソッドオーバーライド

/ 29日目/ 6週目・作業週 コメント

mixinでのメソッドオーバーライドの検証

上の記事で、メソッドオーバーライドが予想通りに動かなくて悩んでいたのだけれど、以下のように子クラスへのincludeを先に書くと、想定の動作となった。

Original::Successor.class_eval do
  include Override1::Klass
end

Original::Klass.class_eval do
  include Override1::Klass
end

Original::Klass.new.overridden #=> 'original'
Original::Successor.new.overridden #=> 'override 1'

上記記事で取り組んでいたGemのソースも確かに子クラスへのincludeが先に書かれている。 親クラスに先にincldeしてしまうと、子クラスへのincludeが無効になってしまうよう。

まさか順番の違いが原因だとは思わなかったなあ。灯台下暗し。 なぜこういう動作になるのかまだよくわかってないが、もうちょっと調べてみよう。

あと、Ruby2.0からはこんな場合にprependを使うといいらしい。というか、alias_method_chain使うのが普通なのか。PullReq送りなおしてみるか・・・。

上記リンク、関数検索の優先順位についても書かれていて勉強になる。

今週は作業週だったんだけれど体調を崩してしまって半分くらいしか活動できなかった。kowabanaの開発は勉強になることが多いのでちょともったいない。

[追記:2013-11-30]

上記の動作の原因を追ってみた。まず、Class::ancestorでクラスの継承・Mixin関係を見ることができるみたいなので、子クラス、親クラスのinclude順、それぞれの場合で見てみる。以前の検証コードよりも簡略化している。

まずは、親クラスに先にincludeする場合。

class Original
  def overridden
    'original'
  end
end

class Successor < Original
end

module Override1
  def overridden
    'override 1'
  end
end

#親クラスに先にinclude
Original.class_eval do
  include Override1
end

Successor.class_eval do
  include Override1
end

Original.ancestors # => [Original, Override1, Object, Kernel, BasicObject]
Successor.ancestors # => [Successor, Original, Override1, Object, Kernel, BasicObject]

次に、子クラスに先にincludeする場合。

class Original
  def overridden
    'original'
  end
end

class Successor < Original
end

module Override1
  def overridden
    'override 1'
  end
end

#子クラスに先にinclude
Successor.class_eval do
  include Override1
end

Original.class_eval do
  include Override1
end

Original.ancestors # => [Original, Override1, Object, Kernel, BasicObject]
Successor.ancestors # => [Successor, Override1, Original, Override1, Object, Kernel, BasicObject]

Successorの継承関係に注目。

# 親クラスに先にinclude
Successor.ancestors # => [Successor, Original, Override1, Object, Kernel, BasicObject]
# 子クラスに先にinclude 二つ目のOverride1が追加されている
Successor.ancestors # => [Successor, Override1 , Original, Override1, Object, Kernel, BasicObject]

やっぱり、先にincludeするのが子クラスか親クラスによってメソッドの探索順が変わってくるよう。

このへん、rubyのソース見たら何か分かるのかなーと思ってソースを追ってみたら、そのものズバリのコメントがあった。

親クラスにincludeされているモジュールは子クラスでincludeできない仕様となっているよう。 このコメントと仕様を実現するロジックが加えられたコミットは次になる(Git)。

commit 6f1c934bc361ec5d01a0b4b1a45d07840af02dc3
Author: matz <matz@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>
Date:   Tue Nov 7 08:56:18 2006 +0000

    * class.c (rb_include_module): revert duplicate inclusio
      modules.  [ruby-dev:29793]

ruby-devのスレッドに仕様の議論がある。

コメント