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

Ruby의 책임감 있는 원숭이 패치

2011년에 처음으로 Ruby 코드를 전문적으로 작성하기 시작했을 때 언어에 대해 가장 인상 깊었던 것 중 하나는 유연성이었습니다. Ruby와 함께라면 모든 것이 가능한 것처럼 느껴졌습니다. C# 및 Java와 같은 언어의 경직성에 비해 Ruby 프로그램은 거의 살아 있는 것처럼 보였습니다. .

Ruby 프로그램에서 얼마나 많은 놀라운 일을 할 수 있는지 생각해 보십시오. 메소드를 마음대로 정의하고 삭제할 수 있습니다. 존재하지 않는 메소드를 호출할 수 있습니다. 당신은 허공에서 전체 이름 없는 클래스를 불러낼 수 있습니다. 정말 야생입니다.

하지만 그것으로 이야기가 끝나는 것은 아닙니다. 이러한 기술을 자신의 코드에 적용할 수 있지만 Ruby를 사용하면 가상 머신에 로드된 모든 것에 적용할 수도 있습니다. 즉, 다른 사람의 코드를 자신의 코드처럼 쉽게 엉망으로 만들 수 있습니다.

몽키패치란 무엇입니까?

원숭이 패치를 입력하세요. .

간단히 말해서, 원숭이는 기존 코드를 "원숭이"로 패치합니다. 기존 코드는 gem이나 Ruby 표준 라이브러리의 코드와 같이 직접 액세스할 수 없는 코드인 경우가 많습니다. 패치는 일반적으로 버그 수정, 성능 향상 등을 위해 원래 코드의 동작을 변경하도록 설계되었습니다.

가장 정교하지 않은 원숭이 패치는 루비 클래스를 다시 열고 메서드를 추가하거나 재정의하여 동작을 수정합니다.

이 재개방 아이디어는 Ruby의 객체 모델의 핵심입니다. Java에서 클래스는 한 번만 정의할 수 있지만 Ruby 클래스(및 해당 문제에 대한 모듈)는 여러 번 정의할 수 있습니다. 클래스를 두 번째, 세 번째, 네 번째 등으로 정의할 때 우리는 재개방한다고 말합니다. 그것. 우리가 정의하는 모든 새 메서드는 기존 클래스 정의에 추가되며 해당 클래스의 인스턴스에서 호출할 수 있습니다.

이 짧은 예는 수업 재개방 개념을 보여줍니다.

class Sounds
  def honk
    "Honk!"
  end
end
 
class Sounds
  def squeak
    "Squeak!"
  end
end
 
sounds = Sounds.new
sounds.honk    # => "Honk!"
sounds.squeak  # => "Squeak!"

#honk#squeak 메소드는 Sounds에서 사용할 수 있습니다. 다시 여는 마법을 통해 수업을 듣습니다.

기본적으로 monkeypatching은 타사 코드에서 클래스를 다시 여는 행위입니다.

원숭이 패치가 위험한가요?

이전 문장이 당신을 두렵게 했다면, 그것은 아마도 좋은 일입니다. 특히 부주의하게 수행할 때 원숭이 패치는 실제 혼란을 일으킬 수 있습니다.

Array#<<를 재정의하면 어떤 일이 일어날지 잠시 생각해 보십시오. :

class Array
  def <<(*args)
    # do nothing 😈
  end
end

이 네 줄의 코드를 사용하면 전체 프로그램의 모든 단일 배열 인스턴스가 손상됩니다.

또한 #<<의 원래 구현 사라. Ruby 프로세스를 다시 시작하는 것 외에는 되돌릴 방법이 없습니다.

Monkeypatching이 끔찍하게 잘못된 경우

2011년에 저는 저명한 소셜 네트워킹 회사에서 일했습니다. 당시 코드베이스는 Ruby 1.8.7에서 실행되는 대규모 Rails 모노리스였습니다. 수백 명의 엔지니어가 매일 코드베이스에 기여했으며 개발 속도가 매우 빨랐습니다.

어느 시점에서 우리 팀은 String#% 원숭이 패치를 결정했습니다. 국제화 목적으로 복수형을 더 쉽게 쓰기 위해. 다음은 패치가 수행할 수 있는 작업의 예입니다.

replacements = {
  horse_count: 3,
  horses: {
    one: "is 1 horse",
    other: "are %{horse_count} horses"
  }
}
 
# "there are 3 horses in the barn"
"there %{horse_count:horses} in the barn" % replacements

우리는 패치를 작성했고 결국 프로덕션에 배포했지만 작동하지 않는다는 것을 발견했습니다. 사용자가 리터럴 %{...}가 있는 문자열을 보고 있었습니다. 멋지게 복수형 텍스트 대신 문자. 이해가 되지 않았다. 패치는 내 랩톱의 개발 환경에서 완벽하게 작동했습니다. 프로덕션에서 작동하지 않는 이유는 무엇입니까?

처음에 우리는 Ruby 자체에서 버그를 발견했다고 생각했지만 나중에야 프로덕션 Rails 콘솔이 개발 중인 Rails 콘솔과 다른 결과를 생성한다는 것을 알게 되었습니다. 두 콘솔 모두 동일한 Ruby 버전에서 실행되었으므로 Ruby 표준 라이브러리의 버그를 배제할 수 있습니다. 다른 일이 일어나고 있었습니다.

며칠간의 고민 끝에 동료는 다른을 추가한 Rails 이니셜라이저를 추적할 수 있었습니다. String#% 구현 우리 중 누구도 전에 본 적이 없다는 것을. 설상가상으로 이 초기 구현에는 버그도 포함되어 있었기 때문에 프로덕션 콘솔에서 본 결과는 Ruby의 공식 문서와 달랐습니다.

그렇다고 이야기가 끝난 것은 아닙니다. 이전의 monkeypatch를 추적하면서 우리는 모두 같은 방법으로 패치를 적용한 다른 세 개도 발견했습니다. 우리는 공포에 질려 서로를 바라보았다. 이것이 어떻게 작동했나요??

결국 우리는 일관성 없는 행동을 Rails의 열렬한 로딩으로 분류했습니다. 개발 단계에서 Rails는 Ruby 파일을 지연 로드합니다. 즉, require일 때만 로드합니다. 디. 그러나 프로덕션 환경에서 Rails는 초기화 시 앱의 모든 Ruby 파일을 로드합니다. 이것은 원숭이 패치에 큰 원숭이 렌치를 던질 수 있습니다.

수업 재개의 결과

이 경우 각각의 원숭이 패치는 String을 다시 열었습니다. 클래스를 만들고 기존 버전의 #%를 효과적으로 대체했습니다. 다른 방법과 함께. 이 접근 방식에는 몇 가지 주요 함정이 있습니다.

  • 마지막 패치가 적용된 "승리", 즉 동작은 로드 순서에 따라 다릅니다.
  • 원래 구현에 액세스할 수 있는 방법이 없습니다.
  • 패치는 감사 추적을 거의 남기지 않으므로 나중에 찾기가 매우 어렵습니다.

당연하게도 우리는 이 모든 것에 부딪쳤을 것입니다.

처음에 우리는 다른 원숭이 패치가 있는 줄도 몰랐습니다. 이기는 방법의 버그로 인해 원래 구현이 손상된 것으로 나타났습니다. 다른 경쟁 패치를 발견했을 때 많은 puts을 추가하지 않고는 어느 패치가 승리했는지 알 수 없었습니다. 진술.

마지막으로, 개발에서 어떤 방법이 이겼는지 발견하더라도 다른 방법이 프로덕션에서 승리할 것입니다. 또한 Ruby 1.8에 멋진 Method#source_location이 없었기 때문에 마지막으로 적용된 패치를 프로그래밍 방식으로 말하기도 어려웠습니다. 우리가 지금 가지고 있는 방법.

나는 무슨 일이 일어나고 있는지 알아내려고 적어도 일주일을 보냈고, 완전히 피할 수 있는 문제를 쫓는 데 본질적으로 시간을 낭비했습니다.

결국 LocalizedString을 도입하기로 결정했습니다. #%가 수반되는 래퍼 클래스 방법. String 원숭이 패치는 다음과 같이 되었습니다.

class String
  def localize
    LocalizedString.new(self)
  end
end

원숭이 패치 실패 시

내 경험상 원숭이 패치는 종종 다음 두 가지 이유 중 하나로 실패합니다.

  • 패치 자체가 손상되었습니다. 위에서 언급한 코드베이스에는 동일한 방법의 여러 경쟁 구현이 있을 뿐만 아니라 "이기는" 방법이 작동하지 않았습니다.
  • 가정이 잘못되었습니다. 호스트 코드가 업데이트되었으며 패치가 더 이상 작성된 대로 적용되지 않습니다.

두 번째 글머리 기호를 더 자세히 살펴보겠습니다.

최상의 계획조차도...

Monkeypatching은 처음에 도달한 것과 같은 이유로 종종 실패합니다. 원래 코드에 액세스할 수 없기 때문입니다. 바로 그 이유 때문에 원본 코드가 사용자 아래에서 변경될 수 있습니다.

앱이 의존하는 gem에서 이 예를 고려하십시오.

class Sale
  def initialize(amount, discount_pct, tax_rate = nil)
    @amount = amount
    @discount_pct = discount_pct
    @tax_rate = tax_rate
  end
 
  def total
    discounted_amount + sales_tax
  end
 
  private
 
  def discounted_amount
    @amount * (1 - @discount_pct)
  end
 
  def sales_tax
    if @tax_rate
      discounted_amount * @tax_rate
    else
      0
    end
  end
end

잠깐, 그건 옳지 않아. 판매세는 할인된 금액이 아닌 전체 금액에 적용되어야 합니다. 프로젝트에 pull 요청을 제출합니다. 관리자가 PR을 병합하기를 기다리는 동안 이 원숭이 패치를 앱에 추가합니다.

class Sale
  private
 
  def sales_tax
    if @tax_rate
      @amount * @tax_rate
    else
      0
    end
  end
end

그것은 완벽하게 작동합니다. 체크인하고 잊어버리면 됩니다.

오랫동안 모든 것이 괜찮습니다. 그러던 어느 날 재무팀에서 회사가 한 달 동안 판매세를 징수하지 않은 이유를 묻는 이메일을 보냅니다.

혼란스러워서 문제를 파헤치기 시작하고 결국 동료 중 한 명이 최근에 Sale이 포함된 gem을 업데이트했음을 알게 됩니다. 수업. 업데이트된 코드는 다음과 같습니다.

class Sale
  def initialize(amount, discount_pct, sales_tax_rate = nil)
    @amount = amount
    @discount_pct = discount_pct
    @sales_tax_rate = sales_tax_rate
  end
 
  def total
    discounted_amount + sales_tax
  end
 
  private
 
  def discounted_amount
    @amount * (1 - @discount_pct)
  end
 
  def sales_tax
    if @sales_tax_rate
      discounted_amount * @sales_tax_rate
    else
      0
    end
  end
end

프로젝트 관리자 중 한 명이 @tax_rate로 이름을 변경한 것 같습니다. @sales_tax_rate에 대한 인스턴스 변수 . 원숭이 패치는 이전 @tax_rate 값을 확인합니다. 항상 nil인 변수 . 오류가 발생하지 않았기 때문에 아무도 눈치채지 못했습니다. 아무 일도 없었다는 듯이 앱이 움츠러들었습니다.

왜 몽키패치인가?

이러한 예를 고려할 때, monkeypatching은 잠재적인 골칫거리가 될 가치가 없는 것처럼 보일 수 있습니다. 그래서 우리가 그것을 하는 이유는 무엇입니까? 제 생각에는 세 가지 주요 사용 사례가 있습니다.

  • 깨지거나 불완전한 타사 코드 수정
  • 개발의 변경 사항 또는 여러 변경 사항을 신속하게 테스트하기 위해
  • 계측 또는 주석 코드로 기존 기능을 래핑하려면

어떤 경우에는 타사 코드의 버그 또는 성능 문제를 해결하는 실행 가능한 방법은 원숭이 패치를 적용하는 것입니다.

하지만 큰 힘에는 큰 책임이 따릅니다.

책임감 있는 원숭이 패치

나는 좋은지 나쁜지 대신 책임을 중심으로 원숭이 패치 대화를 구성하는 것을 좋아합니다. 물론, monkeypatching은 제대로 수행되지 않으면 혼란을 일으킬 수 있습니다. 그러나 어느 정도 주의를 기울이고 부지런히 일을 처리한다면 상황이 적절할 때 손을 뻗지 않을 이유가 없습니다.

다음은 내가 따르려고 하는 규칙 목록입니다.

  1. 명확한 이름으로 모듈에 패치를 래핑하고 Module#prepend를 사용합니다. 적용하려면
  2. 패치가 올바른지 확인하세요.
  3. 패치의 표면적 제한
  4. 자신에게 탈출구를 마련하세요
  5. 과잉 의사소통

이 기사의 나머지 부분에서는 이러한 규칙을 사용하여 Rails의 DateTimeSelector에 대한 monkeypatch를 작성할 것입니다. 따라서 선택적으로 폐기된 필드 렌더링을 건너뜁니다. 이것은 내가 몇 년 전에 Rails에 실제로 적용하려고 했던 변경 사항입니다. 여기에서 자세한 내용을 확인할 수 있습니다.

그러나 monkeypatch를 이해하기 위해 버려진 필드에 대해 많이 알 필요는 없습니다. 하루가 끝나면 build_hidden이라는 단일 메서드를 교체하는 것뿐입니다. 사실상 아무 것도 하지 않는 것과 함께.

시작하겠습니다!

Module#prepend 사용

이전 역할에서 만난 코드베이스에서 String#%의 모든 구현은 String을(를) 다시 열어 적용되었습니다. 수업. 다음은 앞서 언급한 단점의 목록입니다.

  • 패치 코드가 아닌 호스트 클래스 또는 모듈에서 오류가 발생한 것으로 보입니다.
  • 패치에서 정의한 모든 메소드는 기존 메소드를 동일한 이름으로 대체합니다. 즉, 원래 구현을 호출할 방법이 없습니다.
  • 어떤 패치가 적용되었고 따라서 어떤 방법이 "승리"했는지 알 수 있는 방법이 없습니다.
  • 패치는 감사 추적을 거의 남기지 않으므로 나중에 찾기가 매우 어렵습니다.

대신 패치를 모듈로 래핑하고 Module#prepend를 사용하여 적용하는 것이 훨씬 좋습니다. . 이렇게 하면 원래 구현을 자유롭게 호출하고 Module#ancestors를 빠르게 호출할 수 있습니다. 상속 계층 구조에 패치가 표시되므로 문제가 발생하면 더 쉽게 찾을 수 있습니다.

마지막으로 간단한 prepend 어떤 이유로 패치를 비활성화해야 하는 경우 명령문을 쉽게 주석 처리할 수 있습니다.

Rails monkeypatch를 위한 모듈의 시작은 다음과 같습니다.

module RenderDiscardedMonkeypatch
end
 
ActionView::Helpers::DateTimeSelector.prepend(
  RenderDiscardedMonkeypatch
)

올바른 패치

이 기사에서 한 가지를 빼면 다음과 같이 하십시오. 올바른 코드를 패치하고 있다는 것을 모르는 경우 monkeypatch를 적용하지 마십시오. 대부분의 경우 가정이 여전히 유지되는지 프로그래밍 방식으로 확인할 수 있어야 합니다(결국 Ruby입니다). 체크리스트는 다음과 같습니다.

  1. 패치하려는 클래스 또는 모듈이 존재하는지 확인
  2. 메소드가 존재하고 합당한 합이 있는지 확인
  3. 패치하는 코드가 gem에 있는 경우 gem의 버전을 확인하세요.
  4. 가정이 성립하지 않는 경우 유용한 오류 메시지와 함께 구제하세요.

박쥐에서 바로 우리의 패치 코드는 꽤 중요한 가정을 했습니다. ActionView::Helpers::DateTimeSelector라는 상수를 가정합니다. 존재하며 클래스 또는 모듈입니다.

클래스/모듈 확인

패치를 시도하기 전에 상수가 존재하는지 확인합시다:

module RenderDiscardedMonkeypatch
end
 
const = begin
  Kernel.const_get('ActionView::Helpers::DateTimeSelector')
rescue NameError
end
 
if const
  const.prepend(RenderDiscardedMonkeypatch)
end

좋습니다. 하지만 이제 지역 변수(const ) 전역 범위에 포함됩니다. 문제를 해결해 보겠습니다.

module RenderDiscardedMonkeypatch
  def self.apply_patch
    const = begin
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
 
    if const
      const.prepend(self)
    end
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

확인 방법

다음으로 패치된 build_hidden 방법. 또한 존재하고 올바른 수의 인수를 허용하는지 확인하는 검사를 추가해 보겠습니다(즉, 올바른 인수를 가짐). 이러한 가정이 맞지 않으면 뭔가 잘못된 것일 수 있습니다.

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
 
      if const && mtd && mtd.arity == 2
        const.prepend(self)
      end
    end
 
    private
 
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
 
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
  end
 
  def build_hidden(type, value)
    ''
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

보석 버전 확인

마지막으로 올바른 버전의 Rails를 사용하고 있는지 확인하겠습니다. Rails가 업그레이드되면 패치도 업데이트(또는 완전히 제거)해야 할 수 있습니다.

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
 
      if const && mtd && mtd.arity == 2 && rails_version_ok?
        const.prepend(self)
      end
    end
 
    private
 
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
 
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
 
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
 
  def build_hidden(type, value)
    ''
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

유용한 구제금융

인증 코드가 기대와 현실 사이의 불일치를 발견하면 오류를 발생시키거나 최소한 유용한 경고 메시지를 인쇄하는 것이 좋습니다. 여기에서 아이디어는 문제가 있는 것 같을 때 귀하와 동료에게 경고하는 것입니다.

Rails 패치를 수정하는 방법은 다음과 같습니다.

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
 
      unless const && mtd && mtd.arity == 2
        raise "Could not find class or method when patching "\
          "ActionView's date_select helper. Please investigate."
      end
 
      unless rails_version_ok?
        puts "WARNING: It looks like Rails has been upgraded since "\
          "ActionView's date_select helper was monkeypatched in "\
          "#{__FILE__}. Please reevaluate the patch."
      end
 
      const.prepend(self)
    end
 
    private
 
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
 
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
 
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
 
  def build_hidden(type, value)
    ''
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

표면적 제한

원숭이 패치에서 도우미 메서드를 정의하는 것이 완벽하게 무해해 보일 수 있지만 Module#prepend를 통해 정의된 모든 메서드는 상속의 마법을 통해 기존 것을 무시합니다. 호스트 클래스나 모듈이 특정 메서드를 정의하지 않는 것처럼 보일 수 있지만 확실히 알기는 어렵습니다. 이러한 이유로 저는 패치하려는 방법만 정의하려고 합니다.

이것은 객체의 싱글톤 클래스에 정의된 메서드, 즉 class << self 내부에 정의된 메서드에도 적용됩니다. .

#build_hidden 하나만 교체하도록 Rails 패치를 수정하는 방법은 다음과 같습니다. 방법:

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
 
      unless const && mtd && mtd.arity == 2
        raise "Could not find class or method when patching"\
          "ActionView's date_select helper. Please investigate."
      end
 
      unless rails_version_ok?
        puts "WARNING: It looks like Rails has been upgraded since"\
          "ActionView's date_selet helper was monkeypatched in "\
          "#{__FILE__}. Please reevaluate the patch."
      end
 
      const.prepend(InstanceMethods)
    end
 
    private
 
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
 
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
 
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
 
  module InstanceMethods
    def build_hidden(type, value)
      ''
    end
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

탈출 해치 제공

가능하면 내 monkeypatch의 기능을 옵트인하고 싶습니다. 패치된 코드가 호출되는 위치를 제어할 수 있는 경우에만 옵션입니다. Rails 패치의 경우 @options를 통해 수행할 수 있습니다. DateTimeSelector의 해시 :

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
 
      unless const && mtd && mtd.arity == 2
        raise "Could not find class or method when patching"\
          "ActionView's date_select helper. Please investigate."
      end
 
      unless rails_version_ok?
        puts "WARNING: It looks like Rails has been upgraded since"\
          "ActionView's date_selet helper was monkeypatched in "\
          "#{__FILE__}. Please reevaluate the patch."
      end
 
      const.prepend(InstanceMethods)
    end
 
    private
 
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
 
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
 
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
 
  module InstanceMethods
    def build_hidden(type, value)
      if @options.fetch(:render_discarded, true)
        super
      else
        ''
      end
    end
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

멋진! 이제 발신자는 date_select를 호출하여 옵트인할 수 있습니다. 새로운 옵션의 도우미. 다른 코드 경로는 영향을 받지 않습니다.

date_select(@user, :date_of_birth, {
  order: [:month, :day],
  render_discarded: false
})

과도한 의사소통

내가 당신에게 하는 마지막 조언은 아마도 가장 중요할 것입니다. 당신의 패치가 하는 일과 그것을 재검토해야 할 때를 알리는 것입니다. monkeypatches의 목표는 항상 결국 패치를 완전히 제거하는 것입니다. 이를 위해 책임 있는 monkeypatch에는 다음과 같은 주석이 포함됩니다.

  • 패치의 기능 설명
  • 패치가 필요한 이유 설명
  • 패치의 가정 개요
  • 업데이트된 gem을 가져오는 것과 같이 팀이 대체 솔루션을 재고해야 하는 미래의 날짜를 지정합니다.
  • 관련 pull 요청, 블로그 게시물, StackOverflow 답변 등에 대한 링크 포함

팀이 패치의 가정을 재확인하고 패치가 필요한지 여부를 고려하도록 촉구하기 위해 미리 정해진 날짜에 경고를 인쇄하거나 테스트에 실패할 수도 있습니다.

다음은 Rails date_select의 최종 버전입니다. 패치, 코멘트 및 날짜 확인 완료:

# ActionView's date_select helper provides the option to "discard" certain
# fields. Discarded fields are (confusingly) still rendered to the page
# using hidden inputs, i.e. <input type="hidden" />. This patch adds an
# additional option to the date_select helper that allows the caller to
# skip rendering the chosen fields altogether. For example, to render all
# but the year field, you might have this in one of your views:
#
# date_select(:date_of_birth, order: [:month, :day])
#
# or, equivalently:
#
# date_select(:date_of_birth, discard_year: true)
#
# To avoid rendering the year field altogether, set :render_discarded to
# false:
#
# date_select(:date_of_birth, discard_year: true, render_discarded: false)
#
# This patch assumes the #build_hidden method exists on
# ActionView::Helpers::DateTimeSelector and accepts two arguments.
#
module RenderDiscardedMonkeypatch
  class << self
    EXPIRATION_DATE = Date.new(2021, 8, 15)
 
    def apply_patch
      if Date.today > EXPIRATION_DATE
        puts "WARNING: Please re-evaluate whether or not the ActionView "\
          "date_select patch present in #{__FILE__} is still necessary."
      end
 
      const = find_const
      mtd = find_method(const)
 
      # make sure the class we want to patch exists;
      # make sure the #build_hidden method exists and accepts exactly
      # two arguments
      unless const && mtd && mtd.arity == 2
        raise "Could not find class or method when patching "\
          "ActionView's date_select helper. Please investigate."
      end
 
      # if rails has been upgraded, make sure this patch is still
      # necessary
      unless rails_version_ok?
        puts "WARNING: It looks like Rails has been upgraded since "\
          "ActionView's date_select helper was monkeypatched in "\
          "#{__FILE__}. Please re-evaluate the patch."
      end
 
      # actually apply the patch
      const.prepend(InstanceMethods)
    end
 
    private
 
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
      # return nil if the constant doesn't exist
    end
 
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
      # return nil if the method doesn't exist
    end
 
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
 
  module InstanceMethods
    # :render_discarded is an additional option you can pass to the
    # date_select helper in your views. Use it to avoid rendering
    # "discarded" fields, i.e. fields marked as discarded or simply
    # not included in date_select's :order array. For example,
    # specifying order: [:day, :month] will cause the helper to
    # "discard" the :year field. Discarding a field renders it as a
    # hidden input. Set :render_discarded to false to avoid rendering
    # it altogether.
    def build_hidden(type, value)
      if @options.fetch(:render_discarded, true)
        super
      else
        ''
      end
    end
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

결론

위에서 설명한 제안 중 일부가 과도하게 보일 수 있다는 점을 완전히 이해합니다. Rails 패치에는 실제 패치 코드보다 훨씬 더 방어적인 인증 코드가 포함되어 있습니다!

그 모든 추가 코드를 브로드소드용 칼집으로 생각하십시오. 보호막으로 둘러싸여 있으면 베임을 피하기가 훨씬 쉽습니다.

하지만 정말 중요한 것은 책임감 있는 원숭이 패치를 프로덕션에 배포하는 데 자신이 있다는 것입니다. 무책임한 것은 당신이나 당신의 회사의 시간, 돈, 개발자 건강을 앗아가기 위해 기다리고 있는 시한 폭탄일 뿐입니다.

추신 Ruby Magic 게시물이 언론에 공개되는 즉시 읽고 싶다면 Ruby Magic 뉴스레터를 구독하고 게시물을 놓치지 마세요!