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

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



1
구별과 함께, 나는 그것이 가능하다는 것을 안다. 나는 그것을하는 가장 못생긴 방법과 순진한 선택이 작동하지 않는 이유를 요구하고 있습니다.
Boris Stitnicky

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추가하고 클래스 메서드를 추가합니다. 이것이 작동하는 방식입니다. 나는 만 충족되지 않은 기대 :) 불일치 표시되지 않습니다
세르지오 Tulentsev

1
나는 당신의 제안이이 문제의 실질적인 해결책이 얻는만큼 우아하다는 사실을 천천히 참아 내고 있습니다. 그러나 클래스에서 작동하는 것이 모듈에서 작동하지 않는 이유를 알고 싶습니다.
Boris Stitnicky

6
@BorisStitnicky이 대답을 믿으십시오. 이것은 Ruby에서 매우 일반적인 관용구로, 질문 한 사용 사례와 경험 한 이유를 정확하게 해결합니다. "불법"해 보일 수 있지만 최선의 방법입니다. (이렇게 자주하면 included메소드 정의를 다른 모듈 로 옮기고 해당 모듈을 메인 모듈에 포함시킬 수 있습니다.)
Phrogz

2
"왜?"에 대한 더 많은 통찰력을 위해이 스레드 를 읽으십시오. .
Phrogz

2
@werkshy : 더미 클래스에 모듈을 포함합니다.
Sergio Tulentsev 2015

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!

의 인스턴스에서 메서드를 호출하면 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]

그러나 클래스와 모듈도 단순한 객체가 아닌가? 사실 그들은 그렇습니다! 그것은 그들도 싱글 톤 방법을 가질 수 있다는 것을 의미합니까? 네, 그렇습니다! 그리고 이것이 클래스 메서드가 탄생하는 방법입니다.

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호출을 모듈로 이동

이 이전 예제는 다음 두 가지 이유로 잘 구조화 된 코드가 아닙니다.

  1. 이제 모듈을 올바르게 포함하기 위해 정의 에서 와 둘 다를 호출해야합니다 . 유사한 모듈을 많이 포함해야하는 경우 매우 번거로울 수 있습니다.includeextendHostClass
  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
나는 이것이 내가 지금까지 본 최고의 서면 답변 일 것이라고 생각합니다. 놀라 울 정도로 명확하고 Ruby에 대한 나의 이해를 넓혀 주셔서 감사합니다. 내가 이것을 100pt 보너스를 선물 할 수 있다면 나는 할 것이다!
Peter Nixey

7

세르지오 레일 이미 (또는에 따라 신경 쓰지 않는 사람에 대한 의견에서 언급 한 바와 같이 활동 지원 ), 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 대신 블록의 히어 닥 (문자열) : base.class_eval << - 'END'
폴 도노 휴
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.