Refinementsとクラスの継承を組み合わせた動作を確認する

大した話ではないが、Refinementsについてちょっと実験してみたので、結果をまとめておく。


まず、現状のRefinementsについて整理する。
今のRefinementsはファイルスコープという、微妙に分かりづらいスコープで適用される。とりあえずサンプルコードで確認してみる

# str_refine.rb
module StrRefine
  refine String do
    def hoge
      "str_hoge" + " " + fuga
    end

    def fuga
      "str_fuga"
    end
  end
end
# main.rb
require_relative "str_refine"

using StrRefine

p "".hoge # => "str_hoge str_fuga"


refineとusingはRefinementSpecによるとModule#refineメソッドとmain.usingメソッドとして定義されている。

この、main.usingが結構曲者で、ファイルのトップレベルでしか基本的に利用できないようになっている。そしてusingを実行したファイルでしか、そのRefinementsは効果が無い。自分は最初、感覚的に分かりづらかったが、そのRefinementsで定義されているメソッドを呼んでいるファイルのトップレベルでusingするということ。


更に、るびまの記事によると以下の機能は削られてしまっている。

  • モジュール単位で refinement を有効にする機能。
  • using されたモジュールの refinement を継承する機能。
  • refine で (クラスではなく) モジュールを拡張する機能。
  • using された複数の refinement 間で super を順に呼び出す機能。
  • module_eval や instance_eval で refinement を有効にする機能。


とは言え、中々ピンと来なかったので、継承周りでいくつか実験してみた。


以下のようにStringを継承したサブクラスを定義する。

# my_string.rb
class MyString < String; end


StrRefineにMyStringを追記する

# str_refine.rb
module StrRefine
  refine String do
    def hoge
      "str_hoge" + " " + fuga
    end

    def fuga
      "str_fuga"
    end
  end

  refine MyString do
    def fuga
      "mystr_fuga"
    end
  end
end


main.rbから呼び出してみる。

# main.rb
require_relative "my_string"
require_relative "str_refine"

using StrRefine

p "".hoge           # => "str_hoge str_fuga"
p MyString.new.hoge # => "str_hoge mystr_fuga"
p MyString.new.fuga # => "mystr_fuga"


これはイメージ通りの動作だと思う。fugaがサブクラスでオーバーライドされているし、親クラスのメソッドも呼ぶことができている。


ここで、refineを定義しているモジュールを分けると、微妙にややこしい事になる。

MyStringを拡張しているrefineを別モジュールに定義してみる。

# mystr_refine.rb
module MyStrRefine
  refine MyString do
    def fuga
      "mystr_fuga"
    end
  end
end


main.rbから呼び出してみる。

# main.rb
require_relative "my_string"
require_relative "str_refine"
require_relative "mystr_refine"

using StrRefine
using MyStrRefine

p "".hoge           # => "str_hoge str_fuga"
p MyString.new.hoge # => "str_hoge str_fuga"
p MyString.new.fuga # => "mystr_fuga"


という感じになって、「おや、fugaのオーバーライドが出来ていない」となる。ちなみにこれは、同一ファイル内で複数モジュールを記述しても同様の動作になる。つまりモジュールを分けると、Refinementsは独立した空間になる。

MyString#hogeが呼んでいるのはメソッド探索チェーンにあるrefineされたStringであって、そのRefinementsの世界にはMyString#fugaは存在しない。

これは、逆も然りで、MyString#fugaでsuperを使うと以下のようになる。

module MyStrRefine
  refine MyString do
    def fuga
      "mystr_fuga" + super
    end
  end
end
p MyString.new.hoge # => "str_hoge str_fuga"
p MyString.new.fuga # => super: no superclass method `fuga' for "":MyString (NoMethodError)


MyString#fugaを定義しているRefinementsの世界には、String#fugaは定義されていないため、superclassにメソッドが無いよ、と怒られてしまう。

「using された複数の refinement 間で super を順に呼び出す機能。」ってこういう事なのかな。

ファイルスコープ云々とは、どうも別物のようで、StrRefineを定義しているファイル上で、MyStrRefineをusingしても、モジュールが別だとお互いのメソッドには触れられないようになっている。

一方で、モジュールの再オープンはオッケーみたいなので、ファイルを別々に分けても、モジュール名さえ同一なら、適切に処理できるようだ。


まあ、何でこんなことやりたいかと言うと、やたらと柔軟性を求められるシステム作っていて、サブシステムごとに親クラスの動作を使いまわしつつ、Refinementsで要所だけ上書きできんかなー、と思って色々実験してみたからです。


やっぱ、ややこしいなあ…。どこのメソッド呼ばれてるのか、ちゃんと気を付けないといけない。振舞いを特定の関心事の世界に閉じ込められるのは結構ありがたいんだけど。


多分、今やりたい事は、同一の関心事に対するRefinementsを一つのモジュールにまとめて、個別に拡張したい時にはモジュールを再オープンして定義を追加すればいけそうな気がする。モジュールの定義とusingまでのロード順はちゃんとやらないとハマりそうだけど。


追記: 5/7 15:30
想像通りといえば想像通りなんだけど、ActiveSupport::Concernなんかを使って、こういう風に書けばそれっぽく名前分けたりとか出来そう。

# mystr_refine.rb
module MyStrRefine
  extend ActiveSupport::Concern

  included do
    refine MyString do
      def fuga
        "mystr_fuga" + super
      end
    end
  end
end
# main.rb
require_relative "my_string"
require_relative "refine"

StrRefine.send(:include, MyStrRefine)

using StrRefine

p "".hoge
p MyString.new.hoge
p MyString.new.fuga

|