Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Rails 5, Module#prepend 및 `Alias_method_chain`의 끝

Rails 4.2 발표에는 곧 출시될 Rails 5에 대한 흥미로운 소식이 있었습니다. 아마도 Ruby 2.2가 필요할 것입니다. Ruby 2의 모든 장점을 활용하는 최초의 Rails 버전이 됩니다.

게시물은 가비지 수집 기호 및 키워드 인수를 언급했습니다. 하지만 나에게 가장 흥미로운 Ruby 2 기능 중 하나는 Module#prepend입니다.

alias_method_chain의 좋은 점과 나쁜 점

내가 Rails를 처음 배웠을 때 alias_method_chain 내 마음을 완전히 날려 버렸어. Ruby가 얼마나 유연한지 보여주었습니다.

단 한 줄의 코드로 메소드 작동 방식을 완전히 바꿀 수 있습니다. 더 이상 원하는 코드를 추가하기 위해 라이브러리를 해킹할 필요가 없습니다. 바로 추가할 수 있습니다. alias_method_chain gem에 대한 첫 번째 패치로 이어졌고, 이는 첫 번째 pull 요청으로 이어졌고, 이는 첫 번째 오픈 소스 기여로 이어졌습니다.

하지만 원숭이 패치와 마찬가지로 alias_method_chain 과도하게 사용되었고 문제가 명백해지기 시작했습니다.

  • 생성하는 메서드 이름은 혼란스러워 오류를 찾고 디버그하기가 어렵습니다. 예:
class Person
  def greeting
    "Hello"
  end
end

module GreetingWithExcitement
  def self.included(base)
    base.class_eval do
      alias_method_chain :greeting, :excitement
    end
  end

  def greeting_with_excitement
    "#{greeting_without_excitement}!!!!!"
  end
end

Person.send(:include, GreetingWithExcitement)

Person#greeting에 오류가 있는 경우 , 역추적은 Person#greeting_without_excitement에서 실제로 오류가 발생했음을 알려줍니다. . 그러나 그 방법은 어디에 정의되어 있습니까? 어디에서도 볼 수 없습니다. 어떤 greeting인지 어떻게 알 수 있나요? 메서드에 버그가 있습니까? 그리고 메서드 이름은 연결할수록 더 혼란스러워집니다.

  • alias_method_chain을 호출하는 경우 같은 클래스에서 같은 매개변수로 두 번 사용하면 스택 오버플로가 발생할 수 있습니다. (이유를 알 수 있습니까?) require가 있는 한 일반적으로 발생하지 않습니다. 문은 사용하는 경로에 대해 일관성이 있습니다. 하지만 Rails 콘솔에 코드를 자주 붙여넣는 것은 매우 성가신 일입니다.

  • 그리고 나머지는 Yehuda Katz의 블로그 게시물에 설명되어 있습니다. 이 게시물은 많은 Rails 개발자들이 alias_method_chain을 포기하도록 설득했습니다. 모듈 상속에 찬성합니다.

그렇다면 여전히 사용되는 이유는 무엇입니까?

대부분의 alias_method_chain을 대체할 수 있습니다. 모듈에서 해당 메서드를 재정의하고 해당 모듈을 자식 클래스에 포함합니다. 그러나 이는 클래스 자체가 아닌 수퍼 클래스를 재정의하려는 경우에만 작동합니다. 즉:

class ParentClass
  def log
    puts "In parent"
  end
end

class ChildClass < ParentClass
  def log
    puts "In child"
    super
  end

  def log_with_extra_message
    puts "In child, with extra message"
    log_without_extra_message
  end

  alias_method_chain :log, :extra_message
end

ChildClass.new.log를 실행한 경우 , 다음과 같이 표시됩니다.

In child, with extra message
In child
In parent

alias_method_chain 대신 모듈을 사용하려고 시도한 경우 , 출력을 다음과 같이 얻을 수 있습니다.

In child
In child, with extra message
In parent

하지만 할 수 없습니다. log를 변경하지 않고 원래 출력과 일치 ChildClass의 메소드 . Ruby 상속은 그런 식으로 작동하지 않습니다. 그렇지 않았습니다.

Ruby 2.0에서 변경된 사항은 무엇입니까?

Ruby 2.0 이전에는 아래에 코드를 추가할 방법이 없었습니다. 클래스, 그 위에만 있습니다. 하지만 prepend , 모듈의 메서드로 클래스의 메서드를 재정의할 수 있으며 super를 사용하여 클래스 구현에 계속 액세스할 수 있습니다. . 따라서 마지막 예제를 사용하여 다음을 사용하여 원래 출력을 얻을 수 있습니다.

class ParentClass
  def log
    puts "In parent"
  end
end

module ExtraMessageLogging
  def log
    puts "In child, with extra message"
    super
  end
end

class ChildClass < ParentClass
  prepend ExtraMessageLogging
  def log
    puts "In child"
    super
  end
end
In child, with extra message
In child
In parent

완벽합니다.

prepend 여전히 머리를 감는 것이 어렵습니다. 다음과 같이 생각하십시오.

class NewChildClass < ChildClass
  include ExtraMessageLogging
end
  
ChildClass = NewChildClass

클래스 이름을 엉망으로 만들지 않고 이미 존재하는 개체에 영향을 미친다는 점을 제외하고.

(예, Ruby에서 클래스 이름을 다시 할당할 수 있습니다. 아니요, 아마도 좋은 생각이 아닐 것입니다.)

이것이 Rails에 의미하는 바는 무엇입니까?

그래서 alias_method_chain을 사용한 마지막 변명은 Ruby 2.0에서 사라졌습니다. alias_method_chain의 남은 몇 가지 예 중 하나를 들 수 있습니다. 레일스:

rails/activesupport/lib/active_support/core_ext/range/each.rb
require 'active_support/core_ext/module/aliasing'

class Range #:nodoc:

  def each_with_time_with_zone(&block)
    ensure_iteration_allowed
    each_without_time_with_zone(&block)
  end
  alias_method_chain :each, :time_with_zone

  def step_with_time_with_zone(n = 1, &block)
    ensure_iteration_allowed
    step_without_time_with_zone(n, &block)
  end
  alias_method_chain :step, :time_with_zone

  private
  def ensure_iteration_allowed
    if first.is_a?(Time)
      raise TypeError, "can't iterate from #{first.class}"
    end
  end
end

대신 모듈로 교체하십시오.

require 'active_support/core_ext/module/aliasing'

module RangeWithTimeWithZoneSupport #:nodoc:

  def each(&block)
    ensure_iteration_allowed
    super(&block)
  end

  def step(n = 1, &block)
    ensure_iteration_allowed
    super(n, &block)
  end
  
  private
  def ensure_iteration_allowed
    if first.is_a?(Time)
      raise TypeError, "can't iterate from #{first.class}"
    end
  end
end

Range.send(:prepend, RangeSupportingTimeWithZone)

Range#each가 더 깔끔합니다. 이름이 바뀌지 않고 ensure_iteration_allowed 원숭이 패치가 적용되지 않았습니다.

패치가 아닌 상속 사용

Ruby는 엄청난 유연성을 제공하며 이것이 제가 좋아하는 이유 중 하나입니다. 그러나 강력한 개체 모델도 있습니다. 따라서 자신의 코드를 삽입하려면 해킹하기 전에 모듈과 상속에 기대어 보세요. 코드를 훨씬 더 쉽게 이해하고 디버그할 수 있으며 alias_method_chain과 같이 감지하기 어려운 부작용을 피할 수 있습니다. .

alias_method_chain 내가 Rails에서 소개된 가장 멋진 방법 중 하나였습니다. 그러나 그 날은 셀 수 있습니다. 우리는 그것을 능가했습니다. 그리고 그것이 사라지면 나는 그것을 놓치지 않을 것입니다.