Rubyのモジュール/ミックスインからクラスメソッドを継承する


95

Rubyでは、クラスメソッドが継承されることが知られています。

class P
  def self.mm; puts 'abc' end
end
class Q < P; end
Q.mm # works

ただし、ミックスインでは機能しないのは驚きです。

module M
  def self.mm; puts 'mixin' end
end
class N; include M end
M.mm # works
N.mm # does not work!

#extendメソッドがこれを行うことができることを知っています:

module X; def mm; puts 'extender' end end
Y = Class.new.extend X
X.mm # works

しかし、私はインスタンスメソッドとクラスメソッドの両方を含むミックスイン(または、むしろ書きたい)を書いています。

module Common
  def self.class_method; puts "class method here" end
  def instance_method; puts "instance method here" end
end

今私がしたいのはこれです:

class A; include Common
  # custom part for A
end
class B; include Common
  # custom part for B
end

A、BがCommonモジュールからインスタンスメソッドとクラスメソッドの両方を継承する必要があります。しかし、もちろん、それは機能しません。それで、この継承を単一のモジュールから機能させる秘密の方法はありませんか?

これを2つの異なるモジュールに分割することは、私にはエレガントではないようです。1つは含めるモジュールで、もう1つは拡張するモジュールです。別の可能な解決策はCommon、モジュールの代わりにクラスを使用することです。しかし、これは単なる回避策です。(2つの共通機能セットがCommon1ありCommon2、実際にミックスインが必要な場合はどうなりますか?)クラスメソッドの継承がミックスインから機能しない理由は何かありますか?



1
違いがありますが、ここでは、それが可能であることを知っています。それを行うための最も醜い方法と、ナイーブな選択が機能しない理由を求めています。
Boris Stitnicky、2012年

1
より多くの経験から、モジュールをインクルードすることでインクルーダーのシングルトンクラスにモジュールメソッドが追加された場合、Rubyがプログラマーの意図を推測しすぎてしまうことを理解しました。これは、「モジュールメソッド」が実際にはシングルトンメソッドにすぎないためです。モジュールは、シングルトンメソッドを持つために特別なものではなく、メソッドと定数が定義される名前空間になるために特別なものです。名前空間はモジュールのシングルトンメソッドとはまったく関係がないため、実際には、シングルトンメソッドのクラス継承は、モジュールに欠落しているよりも驚くべきものです。
Boris Stitnicky

回答:


171

一般的なイディオムは、includedフックを使用してそこからクラスメソッドを挿入することです。

module Foo
  def self.included base
    base.send :include, InstanceMethods
    base.extend ClassMethods
  end

  module InstanceMethods
    def bar1
      'bar1'
    end
  end

  module ClassMethods
    def bar2
      'bar2'
    end
  end
end

class Test
  include Foo
end

Test.new.bar1 # => "bar1"
Test.bar2 # => "bar2"

26
includeインスタンスメソッドをextend追加し、クラスメソッドを追加します。これがどのように機能するかです。矛盾は見られず、満たされていない期待だけが見られます:)
セルジオトゥレンツェフ

1
私はあなたの提案がこの問題の実際的な解決策が得るのと同じくらいエレガントであることをゆっくりと我慢しています。しかし、クラスで機能するものがモジュールでは機能しない理由を知っていただければ幸いです。
Boris Stitnicky、2012年

6
@BorisStitnickyこの回答を信頼してください。これはRubyで非常に一般的なイディオムであり、尋ねたユースケースと正確に経験した理由を正確に解決します。「不格好」に見えるかもしれませんが、最善の策です。(これを頻繁に行う場合は、includedメソッド定義を別のモジュールに移動し、それをメインモジュールに含めることができます。)
Phrogz

2
「なぜ?」についての洞察については、このスレッドをお読みください
Phrogz、2012年

2
@werkshy:モジュールをダミークラスに含めます。
Sergio Tulentsev

47

これは完全なストーリーであり、モジュールのインクルードがRubyでのように機能する理由を理解するために必要なメタプログラミングの概念を説明しています。

モジュールが含まれるとどうなりますか?

モジュールをクラスに含めると、モジュールがクラスの祖先に追加されます。ancestorsメソッドを呼び出すことにより、クラスまたはモジュールの祖先を確認できます。

module M
  def foo; "foo"; end
end

class C
  include M

  def bar; "bar"; end
end

C.ancestors
#=> [C, M, Object, Kernel, BasicObject]
#       ^ look, it's right here!

のインスタンスでメソッドを呼び出すとC、Rubyはこの祖先リストのすべての項目を調べて、指定された名前のインスタンスメソッドを見つけます。私たちが含まれているためMCM今の祖先であるCので、我々が呼ぶときfooのインスタンス上でC、Rubyはにその方法を見つけるだろうM

C.new.foo
#=> "foo"

インクルードによってインスタンスまたはクラスメソッドがクラスにコピーされないことに注意しください。インクルードされたモジュールでインスタンスメソッドも探す必要があることをクラスに「メモ」を追加するだけです。

モジュールの「クラス」メソッドはどうですか?

インクルードはインスタンスメソッドのディスパッチ方法を変更するだけなので、モジュールをクラスに含めると、そのインスタンスメソッドはそのクラスでのみ使用可能になります。モジュールの「クラス」メソッドとその他の宣言は、クラスに自動的にコピーされません。

module M
  def instance_method
    "foo"
  end

  def self.class_method
    "bar"
  end
end

class C
  include M
end

M.class_method
#=> "bar"

C.new.instance_method
#=> "foo"

C.class_method
#=> NoMethodError: undefined method `class_method' for C:Class

Rubyはクラスメソッドをどのように実装しますか?

Rubyでは、クラスとモジュールはプレーンオブジェクトです。これらはクラスClassとのインスタンスですModule。つまり、動的に新しいクラスを作成したり、変数に割り当てたりすることができます。

klass = Class.new do
  def foo
    "foo"
  end
end
#=> #<Class:0x2b613d0>

klass.new.foo
#=> "foo"

Rubyでは、オブジェクトにいわゆるシングルトンメソッドを定義することもできます。これらのメソッドは、オブジェクトの特別な非表示のシングルトンクラスに新しいインスタンスメソッドとして追加されます。

obj = Object.new

# define singleton method
def obj.foo
  "foo"
end

# here is our singleton method, on the singleton class of `obj`:
obj.singleton_class.instance_methods(false)
#=> [:foo]

しかし、クラスとモジュールも単なるオブジェクトではありませんか?実際にはそうです!つまり、シングルトンメソッドも使用できるということですか。はい、そうです!そして、これがクラスメソッドが生まれる方法です:

class Abc
end

# define singleton method
def Abc.foo
  "foo"
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

または、クラスメソッドを定義するより一般的な方法は、self作成されるクラスオブジェクトを参照するクラス定義ブロック内で使用することです。

class Abc
  def self.foo
    "foo"
  end
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

モジュールにクラスメソッドを含めるにはどうすればよいですか?

先ほど確立したように、クラスメソッドは実際にはクラスオブジェクトのシングルトンクラスの単なるインスタンスメソッドです。これは、モジュールをシングルトンクラスインクルードして、一連のクラスメソッドを追加できることを意味しますか?はい、そうです!

module M
  def new_instance_method; "hi"; end

  module ClassMethods
    def new_class_method; "hello"; end
  end
end

class HostKlass
  include M
  self.singleton_class.include M::ClassMethods
end

HostKlass.new_class_method
#=> "hello"

このself.singleton_class.include M::ClassMethods行は見栄えがよくないので、Rubyを追加しましたObject#extend。これは同じことを行います。つまり、オブジェクトのシングルトンクラスにモジュールを含めます。

class HostKlass
  include M
  extend M::ClassMethods
end

HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
#    ^ there it is!

extend通話をモジュールに移動する

この前の例は、2つの理由から、適切に構造化されたコードではありません。

  1. 私たちは今、呼び出す必要があり、両方を includeし、extend中にHostClass私たちのモジュールが正常に含まれ得るために定義。多くの同様のモジュールを含める必要がある場合、これは非常に面倒になる可能性があります。
  2. HostClass直接参照M::ClassMethodsされ、実装の詳細モジュールの知っているかを気にする必要はありません。MHostClass

では、これについてはどうでしょう。include最初の行を呼び出すと、モジュールにそれが含まれていることが何らかの形で通知され、さらにクラスオブジェクトが渡されるので、それextend自体を呼び出すことができます。このように、必要に応じてクラスメソッドを追加するのはモジュールの仕事です。

これがまさに特別なself.included方法の目的です。Rubyは、モジュールが別のクラス(またはモジュール)に含まれる場合は常にこのメソッドを自動的に呼び出し、最初の引数としてホストクラスオブジェクトを渡します。

module M
  def new_instance_method; "hi"; end

  def self.included(base)  # `base` is `HostClass` in our case
    base.extend ClassMethods
  end

  module ClassMethods
    def new_class_method; "hello"; end
  end
end

class HostKlass
  include M

  def self.existing_class_method; "cool"; end
end

HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
#    ^ still there!

もちろん、クラスメソッドを追加することだけができるわけではありませんself.included。クラスオブジェクトがあるので、他の(クラス)メソッドを呼び出すことができます。

def self.included(base)  # `base` is `HostClass` in our case
  base.existing_class_method
  #=> "cool"
end

2
素晴らしい答え!苦闘の末、ようやくコンセプトを理解することができました。ありがとうございました。
Sankalp

1
これは、私がSOでこれまでに見た中で最も優れた回答であると思います。信じられないほどの明快さと、Rubyの私の理解を広げてくれてありがとう。これを100ptのボーナスとしてプレゼントできたら!
Peter Nixey

7

Sergioがコメントで述べたように、すでにRailsを使用している人(またはActive Supportに依存してもかまわない人)は、Concernここで役に立ちます。

require 'active_support/concern'

module Common
  extend ActiveSupport::Concern

  def instance_method
    puts "instance method here"
  end

  class_methods do
    def class_method
      puts "class method here"
    end
  end
end

class A
  include Common
end

3

これを行うと、ケーキを手に入れて食べることができます。

module M
  def self.included(base)
    base.class_eval do # do anything you would do at class level
      def self.doit #class method
        @@fred = "Flintstone"
        "class method doit called"
      end # class method define
      def doit(str) #instance method
        @@common_var = "all instances"
        @instance_var = str
        "instance method doit called"
      end
      def get_them
        [@@common_var,@instance_var,@@fred]
      end
    end # class_eval
  end # included
end # module

class F; end
F.include M

F.doit  # >> "class method doit called"
a = F.new
b = F.new
a.doit("Yo") # "instance method doit called"
b.doit("Ho") # "instance method doit called"
a.get_them # >> ["all instances", "Yo", "Flintstone"]
b.get_them # >> ["all instances", "Ho", "Flintstone"]

インスタンスとクラス変数を追加するつもりであれば、この方法で実行しない限り、一連の壊れたコードにぶつかるので、結局は髪が抜けてしまいます。


定数の定義、ネストされたクラスの定義、メソッド外でのクラス変数の使用など、class_evalをブロックに渡すときに機能しない奇妙なことがいくつかあります。これらをサポートするために、class_evalにブロックの代わりにheredoc(文字列)を与えることができます:base.class_eval <<-'END'
Paul Donohue
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.