呼び出すメソッドによってMix-inするモジュールを切り替える


ふと思いました。同じクラス内において、メソッドごとにMix-inするモジュールを切り替えることはできないのかと。

例えば以下のように3つのモジュールがあります。

```rb
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つのメソッドを持ったクラスが与えられます。

```rb
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してみます。

```rb
class Sample
  def method_a
    include AppendA
  end

  def method_b
    include AppendB
  end

  def method_c
    include AppendC
  end
end
```

```rb
s = Sample.new
s.method_a
#  `method_a': undefined method `include' for #<Sample:0x000000031c9488> (NoMethodError)
```

NoMethodErrorになってしまいました。メソッド内からincludeは使えないようです。ちなみに、"Object.include" でも無理でした。

## TRY2. Procでincludeを呼び出す

includeをグローバルな領域で呼び、メソッドによってincludeするモジュールを切り替える方法を検討します。

```rb
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
```

```rb
s = Sample.new
s.method_a
# -> hello world
```

今度は成功しました！・・・・・と思ったのですが、これではダメです。試しに、インスタンスを二つ作成して、メソッドを交互に呼び出します。

```rb
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された状態となっています。

```rb
class Sample
  include AppendA
  include AppendC
  ~
  中略
```

単純に後からincludeされたモジュールに含まれるメソッドで上書きされるなら問題ないのですが、そこまで単純な話ではないかもしれません。

どのような危険を孕んでいるか測れないため、この方法はあまり使いたくありません。

## TRY3. メソッド内でextendする

次に、メソッド内でextendしてみました。

```rb
class Sample
  def method_a
    extend AppendA
    hello
  end

  def method_b
    extend AppendB
    hello
  end

  def method_c
    extend AppendC
    hello
  end
end
```

```rb
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した後は、以下のような状態になるはずです。

```rb
class Sample
  def method_a
    class << self
      def hello
        puts 'hello world'
      end
    end
    hello
  end
  ~
  中略
```

あるいは、

```rb
class Sample
  def method_a
    def self.hello
      puts 'hello world'
    end
    hello
  end
~
中略
```

こうですね。
上記のmethod_aは、いずれも正常に実行できます。ネストされたメソッドは、外側のメソッドが実行される際に定義されるはずなので、実質的には下記のような形になると考えました。

```rb
class Sample

  def self.hello
    puts 'self hello'
  end
  
  def method_a
    hello
  end
  ~
  中略
```

しかし、これでインスタンスを作成したあとにmethod_aを呼ぶと、NameErrorで怒られます。

```rb
s = Sample.new
s.method_a
# -> `method_a': undefined local variable or method `hello' for #<Sample:0x000000030ece70> (NameError)
```

当然と言えば当然です、このhelloはクラスメソッドですから。

なのに、先ほども示したこの形

```rb
class Sample
  def method_a
    def self.hello
      puts 'hello world'
    end
    hello
  end
~
中略
```

や、以下の形なら正常に実行できます。

```rb
class Sample
  def method_a
    def hello
      puts 'hello world'
    end
    hello
  end
~
中略
```

なぜ？

「Mix-inするモジュールをメソッドによって切り替える」という目的は達成できたけど、extendやネストメソッドの理解が不十分なので仕組みが分からない・・・