LoginSignup
1
0

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-09-12

ふと思いました。同じクラス内において、メソッドごとに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やネストメソッドの理解が不十分なので仕組みが分からない・・・

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0