program tip

Ruby의 모듈 / 믹스 인에서 클래스 메서드 상속

radiobox 2020. 9. 9. 07:53
반응형

Ruby의 모듈 / 믹스 인에서 클래스 메서드 상속


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모듈 에서 인스턴스 및 클래스 메서드를 모두 상속 받고 싶습니다 . 그러나 물론 그것은 작동하지 않습니다. 그렇다면 단일 모듈에서이 상속을 작동시키는 비밀 방법이 있지 않습니까?

이것을 하나는 포함하고 다른 하나는 확장하는 두 개의 다른 모듈로 나누는 것은 나에게 좋지 않은 것 같습니다. 또 다른 가능한 해결책은 Common모듈 대신 클래스를 사용하는 것입니다. 그러나 이것은 해결 방법 일뿐입니다. (두 세트의 공통 기능이 Common1있고 Common2정말로 믹스 인이 필요하다면?) 믹스 인에서 클래스 메서드 상속이 작동하지 않는 깊은 이유가 있습니까?


일반적인 관용구는 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메소드 를 호출하여 모든 클래스 또는 모듈의 조상을 볼 수 있습니다 .

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!

의 인스턴스에서 메서드를 호출하면 CRuby는 제공된 이름 인스턴스 메서드 를 찾기 위해이 조상 목록의 모든 항목을 살펴 봅니다 . 우리가 포함 된 이후 MC, M지금의 조상이며 C우리가 호출 할 때 그래서, foo인스턴스에 C, 루비는 그 방법을 찾을 수 있습니다 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에서 클래스와 모듈은 일반 객체입니다. 클래스 ClassModule. 즉, 동적으로 새 클래스를 만들고 변수에 할당 할 수 있습니다.

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]

But aren't classes and modules just plain objects as well? In fact they are! Does that mean that they can have singleton methods too? Yes, it does! And this is how class methods are born:

class Abc
end

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

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

Or, the more common way of defining a class method is to use self within the class definition block, which refers to the class object being created:

class Abc
  def self.foo
    "foo"
  end
end

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

How do I include the class methods in a module?

As we just established, class methods are really just instance methods on the singleton class of the class object. Does this mean that we can just include a module into the singleton class to add a bunch of class methods? Yes, it does!

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"

This self.singleton_class.include M::ClassMethods line does not look very nice, so Ruby added Object#extend, which does the same – i.e. includes a module into the singleton class of the object:

class HostKlass
  include M
  extend M::ClassMethods
end

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

Moving the extend call into the module

This previous example is not well-structured code, for two reasons:

  1. We now have to call both include and extend in the HostClass definition to get our module included properly. This can get very cumbersome if you have to include lots of similar modules.
  2. HostClass directly references M::ClassMethods, which is an implementation detail of the module M that HostClass should not need to know or care about.

So how about this: when we call include on the first line, we somehow notify the module that it has been included, and also give it our class object, so that it can call extend itself. This way, it's the module's job to add the class methods if it wants to.

This is exactly what the special self.included method is for. Ruby automatically calls this method whenever the module is included into another class (or module), and passes in the host class object as the first argument:

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!

Of course, adding class methods is not the only thing we can do in self.included. We have the class object, so we can call any other (class) method on it:

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

As Sergio mentioned in comments, for guys who are already in Rails (or don’t mind depending on Active Support), Concern is helpful here:

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

You can have your cake and eat it too by doing this:

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"]

If you intend to add instance, and class variables, you will end up pulling out your hair as you will run into a bunch of broken code unless you do it this way.

참고URL : https://stackoverflow.com/questions/10692961/inheriting-class-methods-from-modules-mixins-in-ruby

반응형