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つの異なるモジュールに分割するのは私には優雅ではないようです。別の可能な解決策は、モジュールの代わりにクラスCommon
を使用することです。しかし、これは単なる回避策です。 (共通の機能Common1
とCommon2
の2つのセットがあり、ミックスインが本当に必要な場合はどうなりますか?)クラスメソッドの継承がミックスインから機能しない理由はありますか?
一般的なイディオムは、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"
モジュールのインクルージョンがRubyのように機能する理由を理解するために必要なメタプログラミングの概念を説明する完全なストーリーを次に示します。
モジュールをクラスに含めると、モジュールがクラスのancestorsに追加されます。 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は、指定されたインスタンスメソッドを見つけるためにこの祖先リストのすべての項目を調べます名前。M
をC
に含めたため、M
は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では、クラスとモジュールはプレーンオブジェクトです。これらはクラスClass
およびModule
のインスタンスです。つまり、新しいクラスを動的に作成したり、変数に割り当てたりすることができます。
klass = Class.new do
def foo
"foo"
end
end
#=> #<Class:0x2b613d0>
klass.new.foo
#=> "foo"
また、Rubyでは、オブジェクトでいわゆるsingleton methodsを定義する可能性があります。これらのメソッドは、オブジェクトの特別な非表示のsingleton classに新しいインスタンスメソッドとして追加されます。
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 added Object#extend
、これは同じです–すなわちモジュールをオブジェクトのシングルトンクラスに含めます。
class HostKlass
include M
extend M::ClassMethods
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ there it is!
extend
呼び出しをモジュールに移動するこの前の例は、次の2つの理由により、適切に構造化されたコードではありません。
include
定義のextend
およびHostClass
を呼び出す必要があります。多数の同様のモジュールを含める必要がある場合、これは非常に面倒になります。HostClass
はM::ClassMethods
を直接参照します。これは、モジュールのimplementation detailM
はHostClass
である必要はありません知っているか気にしています。これについてはどうでしょうか。最初の行でinclude
を呼び出すと、モジュールに含まれていることを何らかの方法で通知し、クラスオブジェクトを提供して、extend
自体を呼び出すことができるようにします。このように、必要に応じてクラスメソッドを追加するのはモジュールの仕事です。
これは、まさにspecial self.included
methodの目的です。 Rubyは、モジュールが別のクラス(またはモジュール)に含まれるたびにこのメソッドを自動的に呼び出し、最初の引数としてHostクラスオブジェクトを渡します。
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
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
これを行うことで、ケーキを食べて食べることもできます。
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"]
インスタンスとクラス変数を追加する場合は、この方法を使用しないと、壊れたコードが大量に発生するため、髪を引っ張ってしまいます。