Computer >> Máy Tính >  >> Lập trình >> Ruby

Monkeypatching có trách nhiệm trong Ruby

Khi tôi lần đầu tiên bắt đầu viết mã Ruby một cách chuyên nghiệp vào năm 2011, một trong những điều khiến tôi ấn tượng nhất về ngôn ngữ này là tính linh hoạt của nó. Có vẻ như với Ruby, mọi thứ đều có thể xảy ra. So với sự cứng nhắc của các ngôn ngữ như C # và Java, các chương trình Ruby gần như giống như chúng vẫn còn sống .

Hãy xem xét có bao nhiêu điều đáng kinh ngạc bạn có thể làm trong một chương trình Ruby. Bạn có thể xác định và xóa các phương thức tùy ý. Bạn có thể gọi các phương thức không tồn tại. Bạn có thể gợi ra toàn bộ các lớp không tên trong không khí loãng. Nó hoàn toàn hoang dã.

Nhưng đó không phải là nơi câu chuyện kết thúc. Mặc dù bạn có thể áp dụng các kỹ thuật này bên trong mã của riêng mình, nhưng Ruby cũng cho phép bạn áp dụng chúng cho bất kỳ thứ gì được tải vào máy ảo. Nói cách khác, bạn có thể xáo trộn mã của người khác dễ dàng như chính bạn có thể.

Monkeypatches là gì?

Nhập Monkeypatch .

Trong ngắn hạn, Monkeypatch "con khỉ với" mã hiện có. Mã hiện tại thường là mã bạn không có quyền truy cập trực tiếp, chẳng hạn như mã từ một viên ngọc hoặc từ thư viện chuẩn Ruby. Các bản vá thường được thiết kế để thay đổi hành vi của mã gốc nhằm sửa lỗi, cải thiện hiệu suất, v.v.

Các Monkeypatch không phức tạp nhất sẽ mở lại các lớp ruby ​​và sửa đổi hành vi bằng cách thêm hoặc ghi đè các phương thức.

Ý tưởng mở lại này là cốt lõi của mô hình đối tượng của Ruby. Trong khi trong Java, các lớp chỉ có thể được định nghĩa một lần, các lớp Ruby (và các mô-đun cho vấn đề đó) có thể được định nghĩa nhiều lần. Khi chúng tôi xác định một lớp lần thứ hai, thứ ba, thứ tư, v.v., chúng tôi nói rằng chúng tôi đang mở lại nó. Bất kỳ phương thức mới nào mà chúng tôi xác định đều được thêm vào định nghĩa lớp hiện có và có thể được gọi trên các phiên bản của lớp đó.

Ví dụ ngắn này minh họa khái niệm mở lại lớp:

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

Lưu ý rằng cả #honk#squeak có sẵn trên Sounds lớp thông qua phép thuật mở lại.

Về cơ bản, Monkeypatching là hành động mở lại các lớp trong mã của bên thứ ba.

Monkeypatching có nguy hiểm không?

Nếu câu trước khiến bạn sợ hãi, đó có lẽ là một điều tốt. Khớp nối, đặc biệt là khi thực hiện bất cẩn, có thể gây ra hỗn loạn thực sự.

Hãy xem xét một chút điều gì sẽ xảy ra nếu chúng ta xác định lại Array#<< :

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

Với bốn dòng mã này, mọi phiên bản mảng đơn lẻ trong toàn bộ chương trình giờ đã bị hỏng.

Hơn nữa, việc triển khai ban đầu của #<< đã biến mất. Ngoài việc khởi động lại quy trình Ruby, không có cách nào để lấy lại nó.

Khi Monkeypatching sai lầm khủng khiếp

Trở lại năm 2011, tôi làm việc cho một công ty mạng xã hội nổi tiếng. Vào thời điểm đó, codebase là một khối Rails khổng lồ chạy trên Ruby 1.8.7. Hàng trăm kỹ sư đã đóng góp vào cơ sở mã hàng ngày và tốc độ phát triển rất nhanh.

Tại một thời điểm, nhóm của tôi đã quyết định bắt cặp String#% để làm cho việc viết số nhiều dễ dàng hơn cho các mục đích quốc tế hóa. Dưới đây là một ví dụ về những gì bản vá của chúng tôi có thể làm:

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

Chúng tôi đã viết bản vá và cuối cùng đưa nó vào sản xuất ... chỉ để thấy rằng nó không hoạt động. Người dùng của chúng tôi đã nhìn thấy các chuỗi có chữ %{...} ký tự thay vì văn bản đa dạng độc đáo. Nó không có ý nghĩa. Bản vá đã hoạt động hoàn toàn tốt trong môi trường phát triển trên máy tính xách tay của tôi. Tại sao nó không hoạt động trong quá trình sản xuất?

Ban đầu, chúng tôi nghĩ rằng chúng tôi đã tìm thấy một lỗi trong chính Ruby, chỉ sau đó, chúng tôi nhận thấy rằng bảng điều khiển Rails sản xuất tạo ra một kết quả khác với bảng điều khiển Rails đang phát triển. Vì cả hai bảng điều khiển đều chạy trên cùng một phiên bản Ruby, chúng tôi có thể loại trừ một lỗi trong thư viện chuẩn của Ruby. Có điều gì đó khác đang xảy ra.

Sau vài ngày vò đầu bứt tai, một đồng nghiệp đã có thể tìm ra bộ khởi tạo Rails đã thêm một triển khai String#% mà không ai trong chúng ta đã từng thấy trước đây. Để làm phức tạp thêm mọi thứ, việc triển khai trước đó cũng có một lỗi, vì vậy kết quả mà chúng tôi thấy trong bảng điều khiển sản xuất khác với tài liệu chính thức của Ruby.

Đó không phải là kết thúc của câu chuyện mặc dù. Khi theo dõi bản vá lỗi khỉ trước đó, chúng tôi cũng tìm thấy không dưới ba bản vá khác, tất cả đều vá cùng một phương pháp. Chúng tôi kinh hoàng nhìn nhau. Làm thế nào điều này đã từng hoạt động ??

Cuối cùng, chúng tôi đã đánh dấu hành vi không nhất quán cho đến khi tải mong muốn của Rails. Trong quá trình phát triển, Rails lười tải các tệp Ruby, tức là chỉ tải chúng khi chúng được require d. Tuy nhiên, trong sản xuất, Rails tải tất cả các tệp Ruby của ứng dụng khi khởi tạo. Điều này có thể ném một cờ lê khỉ lớn vào quá trình bắt khỉ.

Hậu quả của việc mở lại lớp học

Trong trường hợp này, mỗi khóa khỉ mở lại String và thay thế hiệu quả phiên bản hiện tại của #% với một phương pháp khác. Có một số cạm bẫy lớn đối với cách tiếp cận này:

  • Bản vá cuối cùng được áp dụng "thắng", nghĩa là, hành vi phụ thuộc vào thứ tự tải
  • Không có cách nào để truy cập triển khai ban đầu
  • Các bản vá hầu như không để lại dấu vết kiểm tra, điều này khiến chúng rất khó tìm thấy sau này

Có lẽ không ngạc nhiên khi chúng tôi gặp phải tất cả những điều này.

Lúc đầu, chúng tôi thậm chí không biết có những trận đấu khỉ khác đang chơi. Do lỗi trong phương pháp chiến thắng, có vẻ như việc triển khai ban đầu đã bị hỏng. Khi chúng tôi phát hiện ra các bản vá cạnh tranh khác, không thể biết được cái nào đã thắng nếu không thêm các puts phong phú tuyên bố.

Cuối cùng, ngay cả khi chúng tôi phát hiện ra phương pháp nào đã thắng trong quá trình phát triển, thì một phương pháp khác sẽ thắng trong quá trình sản xuất. Về mặt lập trình, rất khó để biết bản vá nào đã được áp dụng lần cuối vì Ruby 1.8 không có Method#source_location tuyệt vời phương pháp chúng tôi hiện có.

Tôi đã dành ít nhất một tuần để tìm hiểu xem chuyện gì đang xảy ra, về cơ bản tôi đã lãng phí thời gian để theo đuổi một vấn đề hoàn toàn có thể tránh được.

Cuối cùng, chúng tôi quyết định giới thiệu LocalizedString lớp wrapper với #% đi kèm phương pháp. String của chúng tôi Monkeypatch sau đó đơn giản trở thành:

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

Khi Monkeypatching không thành công

Theo kinh nghiệm của tôi, các bản Monkeypatch thường không thành công vì một trong hai lý do:

  • Bản thân bản vá đã bị hỏng. Trong codebase mà tôi đã đề cập ở trên, không chỉ có một số cách triển khai cạnh tranh của cùng một phương pháp, mà phương thức "thắng" cũng không hoạt động.
  • Các giả định không hợp lệ. Mã máy chủ đã được cập nhật và bản vá không còn áp dụng như đã viết.

Hãy xem xét tiêu điểm thứ hai chi tiết hơn.

Ngay cả những kế hoạch tốt nhất ...

Monkeypatching thường không thành công vì cùng một lý do mà bạn đã tìm kiếm nó ngay từ đầu - vì bạn không có quyền truy cập vào mã gốc. Chính vì lý do đó, mã gốc có thể thay đổi so với bạn.

Hãy xem xét ví dụ này trong một viên đá quý mà ứng dụng của bạn phụ thuộc vào:

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

Chờ đã, điều đó không đúng. Thuế bán hàng nên được áp dụng cho toàn bộ số tiền, không phải số tiền chiết khấu. Bạn gửi một yêu cầu kéo cho dự án. Trong khi chờ người bảo trì hợp nhất PR của bạn, bạn hãy thêm miếng dán khỉ này vào ứng dụng của mình:

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

Nó hoạt động hoàn hảo. Bạn kiểm tra nó và quên nó đi.

Mọi thứ đều ổn trong một thời gian dài. Sau đó, một ngày nhóm tài chính gửi cho bạn một email hỏi tại sao công ty đã không thu thuế bán hàng trong một tháng.

Bối rối, bạn bắt đầu tìm hiểu vấn đề và cuối cùng nhận thấy một trong những đồng nghiệp của bạn gần đây đã cập nhật viên ngọc có chứa Sale lớp. Đây là mã được cập nhật:

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

Có vẻ như một trong những người bảo trì dự án đã đổi tên @tax_rate biến phiên bản thành @sales_tax_rate . Monkeypatch kiểm tra giá trị của @tax_rate cũ biến, luôn là nil . Không ai để ý vì không có lỗi nào được nêu ra. Ứng dụng hoạt động bình thường như thể không có gì xảy ra.

Tại sao lại sử dụng Monkeypatch?

Với những ví dụ này, có vẻ như bắt khỉ không đáng để bạn phải đau đầu. Vì vậy, tại sao chúng tôi làm điều đó? Theo tôi, có ba trường hợp sử dụng chính:

  • Để sửa mã của bên thứ 3 bị hỏng hoặc không hoàn chỉnh
  • Để nhanh chóng kiểm tra một thay đổi hoặc nhiều thay đổi trong quá trình phát triển
  • Để kết hợp chức năng hiện có bằng mã thiết bị đo đạc hoặc chú thích

Trong một số trường hợp, chỉ cách khả thi để giải quyết lỗi hoặc vấn đề hiệu suất trong mã của bên thứ ba là áp dụng một bản vá lỗi.

Nhưng với sức mạnh to lớn đi kèm với trách nhiệm lớn.

Monkeypatching có trách nhiệm

Tôi thích đóng khung cuộc trò chuyện khỉ ho cò gáy xoay quanh trách nhiệm thay vì điều đó tốt hay xấu. Chắc chắn, tính năng bắt chước có thể gây ra hỗn loạn khi được thực hiện kém. Tuy nhiên, nếu được thực hiện một cách cẩn thận và siêng năng, không có lý do gì để tránh tiếp cận nó khi hoàn cảnh cho phép.

Đây là danh sách các quy tắc tôi cố gắng tuân theo:

  1. Gói bản vá trong một mô-đun có tên rõ ràng và sử dụng Module#prepend để áp dụng nó
  2. Đảm bảo rằng bạn đang vá đúng thứ
  3. Giới hạn diện tích bề mặt của bản vá
  4. Tự tạo cho mình những cửa hầm thoát hiểm
  5. Giao tiếp quá mức

Trong phần còn lại của bài viết này, chúng tôi sẽ sử dụng các quy tắc này để tạo ra một Monkeypatch cho DateTimeSelector của Rails vì vậy nó tùy chọn bỏ qua hiển thị các trường bị loại bỏ. Đây là một thay đổi mà tôi thực sự đã cố gắng thực hiện cho Rails vài năm trước. Bạn có thể tìm thấy thông tin chi tiết tại đây.

Tuy nhiên, bạn không cần phải biết nhiều về các trường bị loại bỏ để hiểu được Monkeypatch. Vào cuối ngày, tất cả những gì nó làm là thay thế một phương pháp duy nhất được gọi là build_hidden với một cái mà hiệu quả không làm gì cả.

Hãy bắt đầu!

Sử dụng Module#prepend

Trong codebase mà tôi đã gặp trong vai trò trước đây của mình, tất cả các triển khai của String#% đã được áp dụng bằng cách mở lại String lớp. Dưới đây là danh sách bổ sung về các nhược điểm mà tôi đã đề cập trước đó:

  • Các lỗi dường như bắt nguồn từ lớp máy chủ hoặc mô-đun thay vì từ mã bản vá
  • Bất kỳ phương thức nào bạn xác định trong bản vá đều thay thế các phương thức hiện có có cùng tên, ý nghĩa, không có cách nào gọi triển khai ban đầu.
  • Không có cách nào để biết bản vá lỗi nào đã được áp dụng và do đó, phương pháp nào "chiến thắng"
  • Các bản vá hầu như không để lại dấu vết kiểm tra, điều này khiến chúng rất khó tìm thấy sau này

Thay vào đó, tốt hơn nhiều là bạn nên bọc bản vá lỗi của mình trong một mô-đun và áp dụng nó bằng cách sử dụng Module#prepend . Làm như vậy giúp bạn có thể tự do gọi triển khai ban đầu và gọi nhanh đến Module#ancestors sẽ hiển thị bản vá trong hệ thống phân cấp kế thừa để dễ dàng tìm thấy nếu có sự cố.

Cuối cùng, một prepend đơn giản rất dễ dàng để đưa ra nhận xét nếu bạn cần tắt bản vá vì lý do nào đó.

Đây là phần bắt đầu của một mô-đun cho Rails khỉpatch của chúng tôi:

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

Vá điều đúng đắn

Nếu bạn bỏ qua một điều từ bài viết này, hãy để nó như sau:không áp dụng một bản vá lỗi khỉ trừ khi bạn biết mình đang vá đúng mã. Trong hầu hết các trường hợp, có thể xác minh theo chương trình rằng các giả định của bạn vẫn được giữ nguyên (suy cho cùng thì đây là Ruby). Đây là danh sách kiểm tra:

  1. Đảm bảo rằng lớp hoặc mô-đun bạn đang cố gắng vá tồn tại
  2. Đảm bảo các phương pháp tồn tại và có độ hiếm phù hợp
  3. Nếu mã bạn đang vá nằm trong một viên ngọc, hãy kiểm tra phiên bản của viên đá quý
  4. Giải cứu với một thông báo lỗi hữu ích nếu các giả định không ổn

Ngay lập tức, mã bản vá của chúng tôi đã đưa ra một giả định khá quan trọng. Nó giả định một hằng số được gọi là ActionView::Helpers::DateTimeSelector tồn tại và là một lớp hoặc mô-đun.

Kiểm tra lớp / mô-đun

Hãy đảm bảo rằng hằng số tồn tại trước khi cố gắng vá nó:

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

Tuyệt vời, nhưng bây giờ chúng tôi đã bị rò rỉ một biến cục bộ (const ) vào phạm vi toàn cầu. Hãy khắc phục điều đó:

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

Phương pháp kiểm tra

Tiếp theo, hãy giới thiệu build_hidden đã được vá phương pháp. Cũng hãy thêm một kiểm tra để đảm bảo rằng nó tồn tại và chấp nhận số lượng đối số phù hợp (tức là có độ hiếm phù hợp). Nếu những giả định đó không đúng, có lẽ đã xảy ra lỗi:

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

Kiểm tra các phiên bản đá quý

Cuối cùng, hãy kiểm tra xem chúng ta có đang sử dụng đúng phiên bản Rails hay không. Nếu Rails được nâng cấp, chúng tôi cũng có thể cần cập nhật bản vá (hoặc loại bỏ nó hoàn toàn).

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

Cứu trợ một cách hữu ích

Nếu mã xác minh của bạn phát hiện ra sự khác biệt giữa kỳ vọng và thực tế, bạn nên nêu ra lỗi hoặc ít nhất là in một thông báo cảnh báo hữu ích. Ý tưởng ở đây là cảnh báo bạn và đồng nghiệp của bạn khi có điều gì đó không ổn.

Đây là cách chúng tôi có thể sửa đổi bản vá Rails của mình:

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

Diện tích bề mặt giới hạn

Mặc dù có vẻ hoàn toàn vô hại khi xác định các phương thức trợ giúp trong một Monkeypatch, hãy nhớ rằng bất kỳ phương thức nào được xác định thông qua Module#prepend sẽ ghi đè những cái hiện có thông qua phép thuật kế thừa. Mặc dù có vẻ như một lớp hoặc mô-đun máy chủ không xác định một phương thức cụ thể, nhưng rất khó để biết chắc chắn. Vì lý do này, tôi cố gắng chỉ xác định các phương pháp tôi định vá.

Lưu ý rằng điều này cũng áp dụng cho các phương thức được xác định trong lớp singleton của đối tượng, tức là các phương thức được định nghĩa bên trong class << self .

Đây là cách sửa đổi bản vá Rails của chúng tôi để chỉ thay thế một bản #build_hidden phương pháp:

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

Tự tạo cho mình những chiếc xe thoát hiểm

Khi có thể, tôi muốn chọn tham gia chức năng của Monkeypatch. Đó chỉ thực sự là một tùy chọn nếu bạn có quyền kiểm soát nơi mã được vá được gọi. Trong trường hợp bản vá Rails của chúng tôi, nó có thể thực hiện được thông qua @options băm trong 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

Tốt đẹp! Giờ đây, người gọi có thể chọn tham gia bằng cách gọi date_select người trợ giúp với tùy chọn mới. Không có đường dẫn nào khác bị ảnh hưởng:

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

Giao tiếp quá mức

Lời khuyên cuối cùng mà tôi dành cho bạn có lẽ là quan trọng nhất - thông báo bản vá của bạn có tác dụng gì và khi nào cần kiểm tra lại. Mục tiêu của bạn với các bản vá lỗi sẽ luôn là cuối cùng loại bỏ bản vá lỗi hoàn toàn. Vì vậy, một Monkeypatch có trách nhiệm bao gồm các nhận xét:

  • Mô tả tác dụng của bản vá
  • Giải thích lý do tại sao bản vá lại cần thiết
  • Phác thảo các giả định mà bản vá đưa ra
  • Chỉ định một ngày trong tương lai khi nhóm của bạn nên xem xét lại các giải pháp thay thế, chẳng hạn như lấy một viên ngọc được cập nhật
  • Bao gồm các liên kết đến các yêu cầu kéo có liên quan, các bài đăng trên blog, câu trả lời của StackOverflow, v.v.

Bạn thậm chí có thể in cảnh báo hoặc không thực hiện được bài kiểm tra vào một ngày định trước để thúc giục nhóm xác nhận lại các giả định của bản vá và xem xét liệu nó có còn cần thiết hay không.

Đây là phiên bản cuối cùng của Rails date_select của chúng tôi vá, hoàn thành với các nhận xét và kiểm tra ngày tháng:

# 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

Kết luận

Tôi hoàn toàn hiểu rằng một số gợi ý mà tôi đã nêu ở trên có vẻ như quá mức cần thiết. Bản vá lỗi Rails của chúng tôi chứa nhiều mã xác minh phòng thủ hơn mã bản vá thực tế!

Hãy coi tất cả mã bổ sung đó như một vỏ bọc cho từ khóa rộng của bạn. Sẽ dễ dàng hơn rất nhiều để tránh bị cắt nếu nó được bao bọc trong một lớp bảo vệ.

Tuy nhiên, điều thực sự quan trọng là tôi cảm thấy tự tin khi triển khai các bản Monkeypatch có trách nhiệm vào sản xuất. Những kẻ vô trách nhiệm chỉ là những quả bom hẹn giờ chờ đợi để tiêu tốn thời gian, tiền bạc và sức khỏe của nhà phát triển cho bạn hoặc công ty của bạn.

Tái bút. Nếu bạn muốn đọc các bài đăng của Ruby Magic ngay khi chúng xuất hiện trên báo chí, hãy đăng ký bản tin Ruby Magic của chúng tôi và không bao giờ bỏ lỡ một bài đăng nào!