원숭이가 인스턴스 메소드를 패치 할 때 새로운 구현에서 재정의 된 메소드를 호출 할 수 있습니까?


444

클래스에서 메소드를 원숭이가 패치한다고 가정하면 어떻게 재정의 메소드에서 재정의 메소드를 호출 할 수 있습니까? 즉 뭔가 조금super

예 :

class Foo
  def bar()
    "Hello"
  end
end 

class Foo
  def bar()
    super() + " World"
  end
end

>> Foo.new.bar == "Hello World"

첫 번째 Foo 클래스는 다른 클래스가 아니어야하고 두 번째 Foo 클래스는 다른 클래스가 아니어야합니까?
Draco Ater

1
아니, 나는 원숭이 패치입니다. 나는 super () 같은 것이 원래의 메소드를 호출하는데 사용할 수 있기를 바랐다
James Hollingworth

1
의 생성 Foo 사용을 제어하지 않는 경우에 필요합니다 Foo::bar. 따라서 방법 을 원숭이 패치 해야합니다.
Halil Özgür

답변:


1165

편집 : 원래이 답변을 쓴 지 9 년이 지났으며 최신 상태로 유지하려면 성형 수술이 필요합니다.

여기 에서 편집하기 전에 마지막 버전을 볼 수 있습니다 .


이름이나 키워드로 덮어 쓴 메소드를 호출 할 수 없습니다 . 그것은 명백히 재정의 된 메소드를 호출 할 있기 때문에 원숭이 패치를 피하고 대신 상속을 선호하는 많은 이유 중 하나입니다 .

원숭이 패치 방지

계승

따라서 가능하다면 다음과 같은 것을 선호해야합니다.

class Foo
  def bar
    'Hello'
  end
end 

class ExtendedFoo < Foo
  def bar
    super + ' World'
  end
end

ExtendedFoo.new.bar # => 'Hello World'

Foo객체 생성을 제어하면 작동 합니다. 를 만드는 모든 장소를 변경 Foo하여 대신를 만듭니다 ExtendedFoo. Dependency Injection Design Pattern , Factory Method Design Pattern , Abstract Factory Design Pattern 또는 그 라인을 따라 무언가 를 사용하는 경우 훨씬 더 효과적 입니다.이 경우 변경해야 할 장소가 있기 때문입니다

대표단

예를 들어 객체의 생성을 제어 하지 않는 경우 Foo(예 :예를 들어 래퍼 디자인 패턴을 사용할 수 있습니다 .

require 'delegate'

class Foo
  def bar
    'Hello'
  end
end 

class WrappedFoo < DelegateClass(Foo)
  def initialize(wrapped_foo)
    super
  end

  def bar
    super + ' World'
  end
end

foo = Foo.new # this is not actually in your code, it comes from somewhere else

wrapped_foo = WrappedFoo.new(foo) # this is under your control

wrapped_foo.bar # => 'Hello World'

기본적으로 Foo객체가 코드에 들어간 시스템 경계에서 다른 객체로 감싸고 코드의 다른 곳에서 원래 객체 대신 해당 객체 를 사용 합니다 .

stdlib Object#DelegateClassdelegate라이브러리에서 헬퍼 메소드를 사용합니다 .

"깨끗한"원숭이 패치

Module#prepend: 믹스 인 추가

위의 두 가지 방법은 원숭이 패치를 피하기 위해 시스템을 변경해야합니다. 이 섹션에서는 시스템을 변경하는 것이 옵션이 아닌 경우 원숭이 패치의 선호되는 최소 침습적 방법을 보여줍니다.

Module#prepend이 사용 사례를 다소 정확하게 지원하기 위해 추가되었습니다. 클래스 바로 아래 의 믹스 인에서 믹스한다는 Module#prepend점을 Module#include제외하고 와 동일한 작업을 수행합니다 .

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  prepend FooExtensions
end

Foo.new.bar # => 'Hello World'

참고 : 나는 또한 Module#prepend이 질문 에 대해 조금 썼다 : Ruby module prepend vs derivation

믹스 인 상속 (파손)

나는 어떤 사람들이 다음과 같은 것을 시도하고 (그리고 왜 이것이 OverOver에서 작동하지 않는지 묻습니다), 즉 include믹스 인 대신 믹싱을하는 것을 보았습니다 prepend.

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end

불행히도, 그것은 작동하지 않습니다. 상속을 사용하므로 사용하는 것이 좋습니다 super. 그러나, Module#include믹스 인 삽입 위의 수단 상속 계층 구조의 클래스, FooExtensions#bar호출되지 않을 것이다 (그리고이 경우에 있었다 라고하는이 super사실을 참조하지 않을 Foo#bar아니라에 Object#bar있기 때문에, 존재하지 않는)을 Foo#bar항상 먼저 발견 될 것이다.

메소드 랩핑

가장 큰 문제는 : 실제 메소드를bar 실제로 유지하지 않고 어떻게 메소드를 유지할 수 있는가? 대답은 자주하는 것처럼 함수형 프로그래밍에 있습니다. 우리는 메소드를 실제 객체 로 보유 하고 클로저 (즉, 블록)를 사용하여 우리 그 객체 보유하도록합니다.

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  old_bar = instance_method(:bar)

  define_method(:bar) do
    old_bar.bind(self).() + ' World'
  end
end

Foo.new.bar # => 'Hello World'

이후이 매우 깨끗 old_bar단지 지역 변수이며,이 클래스 본문의 끝에서 범위 밖으로 갈 것, 어디서나 액세스하는 것은 불가능 반사를 사용! 그리고 이후는 Module#define_method블록을 취하고, 블록 (인 자신의 주변 어휘 환경을여 닫으 우리가 사용하는 define_method대신에 def, 여기) (그리고 단지 는) 아직에 액세스 할 수 있습니다 old_bar그것은 범위 밖으로 사라 후에도.

간단한 설명 :

old_bar = instance_method(:bar)

여기서 우리는 bar메소드를 UnboundMethod메소드 객체 로 감싸서 로컬 변수에 할당합니다 old_bar. 즉, 이제 bar덮어 쓴 후에도 유지할 수있는 방법이 있습니다 .

old_bar.bind(self)

조금 까다 롭습니다. 기본적으로 Ruby (및 거의 모든 단일 디스패치 기반 OO 언어)에서 메소드는 selfRuby 에서 호출되는 특정 수신자 오브젝트에 바인드됩니다 . 즉, 메소드는 항상 호출 된 오브젝트를 알고 있으며 그 오브젝트가 무엇인지 알고 self있습니다. 그러나 우리는 클래스에서 직접 메소드를 가져 왔습니다. 그것이 무엇인지 어떻게 알 수 self있습니까?

음, 우리가해야 할 이유입니다,하지 않는 bind우리를 UnboundMethod반환하며 먼저 객체에 Method우리가 다음 호출 할 수있는 개체를. (을 UnboundMethod모르면 무엇을해야할지 모르기 때문에 호출 할 수 없습니다 self.)

그리고 우리는 무엇을해야 bind합니까? 우리는 단순히 우리 bind자신에게, 원래의 것과 똑같이 행동 bar할 것입니다!

마지막으로에서 Method반환 된 을 호출해야합니다 bind. Ruby 1.9에는 ( .())에 대한 새로운 구문이 있지만 1.8 이상이라면 단순히 call메소드 를 사용할 수 있습니다 . 그것이 .()어쨌든 번역되는 것입니다.

다음은 이러한 개념 중 일부를 설명하는 몇 가지 다른 질문입니다.

"더러운"원숭이 패치

alias_method 체인

원숭이 패치와 관련된 문제는 메소드를 덮어 쓸 때 메소드가 사라져 더 이상 호출 할 수 없다는 것입니다. 이제 백업 사본을 만들어 봅시다!

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  alias_method :old_bar, :bar

  def bar
    old_bar + ' World'
  end
end

Foo.new.bar # => 'Hello World'
Foo.new.old_bar # => 'Hello'

이것의 문제점은 이제 불필요한 old_bar메소드로 네임 스페이스를 오염 시켰다는 것입니다. 이 방법은 설명서에 표시되고 IDE에서 코드 완성에 표시되며 반영하는 동안 표시됩니다. 또한 여전히 호출 할 수는 있지만 처음에는 원숭이의 행동이 마음에 들지 않기 때문에 원숭이가 패치 한 것으로 추정됩니다.

이것이 바람직하지 않은 속성을 가지고 있다는 사실에도 불구하고 불행히도 AciveSupport를 통해 대중화되었습니다 Module#alias_method_chain.

옆으로 : 세분화

전체 시스템이 아닌 일부 특정 장소에서만 다른 동작이 필요한 경우 구체화를 사용하여 원숭이 패치를 특정 범위로 제한 할 수 있습니다. Module#prepend위 의 예제를 사용하여 여기에 설명하겠습니다 .

class Foo
  def bar
    'Hello'
  end
end 

module ExtendedFoo
  module FooExtensions
    def bar
      super + ' World'
    end
  end

  refine Foo do
    prepend FooExtensions
  end
end

Foo.new.bar # => 'Hello'
# We haven’t activated our Refinement yet!

using ExtendedFoo
# Activate our Refinement

Foo.new.bar # => 'Hello World'
# There it is!

이 질문에서 구체화를 사용하는보다 정교한 예를 볼 수 있습니다. 특정 방법에 원숭이 패치를 활성화하는 방법은 무엇입니까?


버려진 아이디어

Ruby 커뮤니티가에 정착하기 전에 Module#prepend이전 토론에서 종종 볼 수있는 여러 가지 아이디어가 떠있었습니다. 이 모두에 의해 가산됩니다 Module#prepend.

방법 조합기

한 가지 아이디어는 CLOS의 메소드 결합기 아이디어였습니다. 이것은 기본적으로 Aspect-Oriented Programming의 아주 작은 버전입니다.

같은 구문 사용

class Foo
  def bar:before
    # will always run before bar, when bar is called
  end

  def bar:after
    # will always run after bar, when bar is called
    # may or may not be able to access and/or change bar’s return value
  end
end

bar메소드 실행을 "연결"할 수 있습니다 .

그러나 bar내에서 반환 값에 액세스하는 방법과 방법은 명확하지 않습니다 bar:after. super키워드를 사용할 수 있을까요?

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar:after
    super + ' World'
  end
end

바꿔 놓음

이전 콤비 prepend네이터는 메서드 super의 맨 에서 호출하는 재정의 메서드로 믹스 인 을 연결 하는 것과 같습니다 . 마찬가지로 애프터 콤비 prepend네이터는 메서드 super의 맨 처음 을 호출하는 재정의 메서드를 사용하여 믹싱 을 수행 하는 것과 같습니다 .

또한 전에 물건을 할 수 호출 한 후 super, 당신은 호출 할 수있는 super여러 번, 모두 검색하고 조작 super하고,의 반환 값을 prepend방법 콤비보다 더 강력한.

class Foo
  def bar:before
    # will always run before bar, when bar is called
  end
end

# is the same as

module BarBefore
  def bar
    # will always run before bar, when bar is called
    super
  end
end

class Foo
  prepend BarBefore
end

class Foo
  def bar:after
    # will always run after bar, when bar is called
    # may or may not be able to access and/or change bar’s return value
  end
end

# is the same as

class BarAfter
  def bar
    original_return_value = super
    # will always run after bar, when bar is called
    # has access to and can change bar’s return value
  end
end

class Foo
  prepend BarAfter
end

old 예어

이 아이디어는 추가하는 새로운 키워드와 유사 super당신이 호출 할 수있는 덮어 같은 방법으로이 방법을 super사용하면 전화를 할 수 있습니다 오버라이드 (override) 방법 :

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'

이것의 주요 문제는 이전 버전과 호환되지 않는다는 것입니다. 메소드가 호출 old되면 더 이상 호출 할 수 없습니다!

바꿔 놓음

superprepended mixin 의 재정의 방법 은 본질적 old으로이 제안 과 동일 합니다.

redef 예어

위와 비슷하지만 덮어 쓴 메소드 를 호출 하고 def홀로 남겨두기 위해 새 키워드를 추가하는 대신 메소드 를 재정의 하기 위한 새 키워드를 추가 합니다. 어쨌든 현재 구문이 잘못되었으므로 이전 버전과 호환됩니다.

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'

새로운 키워드 두 개 를 추가하는 대신 super내부 의미를 다시 정의 할 수도 있습니다 redef.

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

Foo.new.bar # => 'Hello World'

바꿔 놓음

redef메소드를 입력하는 것은 prepended mixin 에서 메소드를 대체하는 것과 같습니다 . super같은 무시 방법 동작합니다에서 super또는 old이 제안한다.


@ Jörg W Mittag, 메소드 래핑 접근 스레드는 안전합니까? 두 개의 동시 스레드 bind가 동일한 old_method변수를 호출하면 어떻게됩니까 ?
Harish Shetty

1
@ KandaBoggu : 나는 당신이 정확히 무엇 을 의미 하는지 알아 내려고 노력 하고 있습니다 :-) 그러나 Ruby의 다른 메타 프로그래밍보다 스레드 안전성이 적다고 확신합니다. 특히, 호출 할 때마다 UnboundMethod#bind새롭고 다른을 반환 Method하므로 다른 스레드에서 동시에 두 번 또는 두 번 호출하는지 여부에 관계없이 충돌이 발생하지 않습니다.
Jörg W Mittag

1
루비와 레일을 시작한 이후로 이런 패치에 대한 설명을 찾고있었습니다. 좋은 답변입니다! 나를 위해 누락 된 유일한 것은 class_eval에 대한 메모와 클래스를 다시 여는 것입니다. 여기 있습니다 : stackoverflow.com/a/10304721/188462
Eugene

1
Ruby 2.0에는 개선 된 블로그
NARKOZ

5
당신은 어디에서 찾을 수 있습니까 oldredef? 내 2.0.0에는 그것들이 없습니다. 아, 루비로 만들지 않은 다른 경쟁 아이디어
마세요

12

앨리어싱 방법을 살펴보십시오.이 방법은 메서드 이름을 새 이름으로 바꿉니다.

자세한 내용과 시작점은이 대체 방법 기사 (특히 첫 번째 부분)를 참조하십시오. 루비 API 문서는 , 또한 (덜 정교한) 예를 제공합니다.


-1

재정의 할 클래스는 원래 메서드가 포함 된 클래스 다음에 다시로드되어야하므로 재정의 require할 파일에서 클래스를 다시로드해야합니다 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.