ふと思いました。同じクラス内において、メソッドごとにMix-inするモジュールを切り替えることはできないのかと。
例えば以下のように3つのモジュールがあります。
module AppendA
def hello
puts 'hello world'
end
end
module AppendB
def hello
puts 'hello ruby'
end
end
module AppendC
def hello
puts 'hello java'
end
end
AppendA、AppendB、AppendCは、いずれもhelloメソッドを持っています。helloメソッドは、メソッド名こそ3つのモジュール間で共通ですが、メソッドの機能は微妙に異なります。
さらに、以下に示すように3つのメソッドを持ったクラスが与えられます。
class Sample
def method_a
# AppendAのhelloメソッドを実行したい
hello
end
def method_b
# AppendBのhelloメソッドを実行したい
hello
end
def method_c
# AppendCのhelloメソッドを実行したい
hello
end
end
method_a、method_b、method_cでは、いずれもレシーバを指定せずに、helloメソッドを呼び出したいとします。ただし、method_aからはAppendA::hello、method_bからはAppendB::helloというように、メソッドによって別のモジュールからhelloメソッドを呼び出します。
どうすれば実現できるのでしょうか。
TRY1. メソッド内でincludeしてみる
試しにメソッド内でincludeしてみます。
class Sample
def method_a
include AppendA
end
def method_b
include AppendB
end
def method_c
include AppendC
end
end
s = Sample.new
s.method_a
# `method_a': undefined method `include' for #<Sample:0x000000031c9488> (NoMethodError)
NoMethodErrorになってしまいました。メソッド内からincludeは使えないようです。ちなみに、"Object.include" でも無理でした。
TRY2. Procでincludeを呼び出す
includeをグローバルな領域で呼び、メソッドによってincludeするモジュールを切り替える方法を検討します。
class Sample
INCLUDE = -> (n) { include n }
def method_a
INCLUDE.call AppendA
hello
end
def method_b
INCLUDE.call AppendB
hello
end
def method_c
INCLUDE.call AppendC
hello
end
end
s = Sample.new
s.method_a
# -> hello world
今度は成功しました!・・・・・と思ったのですが、これではダメです。試しに、インスタンスを二つ作成して、メソッドを交互に呼び出します。
s1 = Sample.new
s1.method_a # -> hello world
s2 = Sample.new
s2.method_c # -> hello java
s1.method_a # -> hello java
s1.method_aを最初に実行した際には、AppendAのhelloメソッドが実行され、"hello word" と出力されています。しかし、s1.method_aを二回目に実行した際には、"hello java" が出力されています。これは、s2.method_cを実行した際のincludeが、s1インスタンスにも影響を与えているためだと思われます。
つまり、内部的には以下のようにインスタンス間共通で、AppendAとAppendCの両方がincludeされた状態となっています。
class Sample
include AppendA
include AppendC
~
中略
単純に後からincludeされたモジュールに含まれるメソッドで上書きされるなら問題ないのですが、そこまで単純な話ではないかもしれません。
どのような危険を孕んでいるか測れないため、この方法はあまり使いたくありません。
TRY3. メソッド内でextendする
次に、メソッド内でextendしてみました。
class Sample
def method_a
extend AppendA
hello
end
def method_b
extend AppendB
hello
end
def method_c
extend AppendC
hello
end
end
s1 = Sample.new
s1.method_a # -> hello world
s2 = Sample.new
s2.method_c # -> hello java
s1.method_a # -> hello world
このパターンだと、想定通りに動作しました。"ancestors" メソッドでincludeしたモジュールを確認しましたが、それぞれのインスタンスで重複したincludeは見られませんでした。
なぜextendで成功したのか
なぜでしょうか?
extendした後は、以下のような状態になるはずです。
class Sample
def method_a
class << self
def hello
puts 'hello world'
end
end
hello
end
~
中略
あるいは、
class Sample
def method_a
def self.hello
puts 'hello world'
end
hello
end
~
中略
こうですね。
上記のmethod_aは、いずれも正常に実行できます。ネストされたメソッドは、外側のメソッドが実行される際に定義されるはずなので、実質的には下記のような形になると考えました。
class Sample
def self.hello
puts 'self hello'
end
def method_a
hello
end
~
中略
しかし、これでインスタンスを作成したあとにmethod_aを呼ぶと、NameErrorで怒られます。
s = Sample.new
s.method_a
# -> `method_a': undefined local variable or method `hello' for #<Sample:0x000000030ece70> (NameError)
当然と言えば当然です、このhelloはクラスメソッドですから。
なのに、先ほども示したこの形
class Sample
def method_a
def self.hello
puts 'hello world'
end
hello
end
~
中略
や、以下の形なら正常に実行できます。
class Sample
def method_a
def hello
puts 'hello world'
end
hello
end
~
中略
なぜ?
「Mix-inするモジュールをメソッドによって切り替える」という目的は達成できたけど、extendやネストメソッドの理解が不十分なので仕組みが分からない・・・