Rails: исправление обезьян ActiveRecord:: Base или создание модуля

Я читаю Rails 4 way (автор Obie Fernandez), известная книга о Rails, и из того, что я читал до сих пор, я может очень рекомендовать его.

Однако есть примерный раздел 9.2.7.1: Несколько методов обратного вызова в одном классе, который меня смущает:

Потерпите меня, чтобы проблема была ясна для всех, я воспроизвел шаги, описанные в этом вопросе в книге.


В разделе описываются обратные вызовы Active Record (before_create, before_update и т.д.) и что можно создать класс, который обрабатывает несколько обратных вызовов для вас. Перечисленный код выглядит следующим образом:

class Auditor
   def initialize(audit_log)
      @audit_log = audit_log
   end

   def after_create(model)
      @audit_log.created(model.inspect)
   end

   def after_update(model)
      @audit_log.updated(model.inspect)
   end

   def after_destroy(model)
      @audit_log.destroyed(model.inspect)
   end
end

В книге говорится, что для добавления этого журнала аудита в класс Active Record вы должны сделать следующее:

class Account < ActiveRecord::Base
   after_create Auditor.new(DEFAULT_AUDIT_LOG)
   after_update Auditor.new(DEFAULT_AUDIT_LOG)
   after_destroy Auditor.new(DEFAULT_AUDIT_LOG)
   ...
end

В книге затем отмечается, что этот код очень уродлив, нужно добавить трех аудиторов на три строки и что он не СУХОЙ. Затем он идет вперед и говорит нам, что для решения этой проблемы мы должны обезвредить метод acts_as_audited в Active Record::Base объект следующим образом:

(книга предлагает поместить этот файл в /lib/core_ext/active_record_base.rb)

class ActiveRecord::Base
   def self.acts_as_audited(audit_log=DEFAULT_AUDIT_LOG)
      auditor = Auditor.new(audit_log)
      after_create auditor
      after_update auditor
      after_destroy auditor
   end
end

который позволяет вам написать класс модели учетных записей следующим образом:

class Account < ActiveRecord::Base
   acts_as_audited
   ...
end

Прежде чем читать книгу, я уже сделал нечто подобное, что добавляет функциональность нескольким моделям Active Record. Техника, которую я использовал, заключалась в создании модуля. Чтобы остаться с примером, то, что я сделал, было похоже на:

(я бы поставил этот файл внутри /app/models/auditable.rb)

module Auditable
   def self.included(base)
      @audit_log = base.audit_log || DEFAULT_AUDIT_LOG #The base class can override it if wanted, by specifying a self.audit_log before including this module
      base.after_create audit_after_create
      base.after_update audit_after_update
      base.after_destroy audit_after_destroy
   end

   def audit_after_create
      @audit_log.created(self.inspect)
   end

   def audit_after_update
      @audit_log.updated(self.inspect)
   end

   def audit_after_destroy
      @audit_log.destroyed(self.inspect)
   end
end

Обратите внимание, что этот файл заменяет метод Auditor и обезглавленный ActiveRecord::Base. Класс Account будет выглядеть следующим образом:

class Account < ActiveRecord::Base
   include Auditable
   ...
end

Теперь вы прочли и то, как это делает книга, и то, как я бы это сделал в прошлом. Мой вопрос: Какая версия более устойчива в долгосрочной перспективе? Я понимаю, что это немного упрямый вопрос, как и все о Rails, но чтобы он был ответственным, я в основном хочу знать:

  • Почему вы хотите обезьян-патч ActiveRecord::Base напрямую, за создание и включение Module?

Ответ 1

Я бы пошел на модуль по нескольким причинам.

Его очевидно; то есть я могу быстро найти код, который определяет это поведение. В acts_as_* я не знаю, если это из какого-то драгоценного камня, кода библиотеки или определенного в этом классе. Там могут быть последствия, связанные с тем, что они переопределены или скопированы в стек вызовов.

Портативный. Он использует вызовы методов, которые обычно определяются в библиотеках, которые определяют обратные вызовы. Возможно, вы можете распространять и использовать эту библиотеку в объектах без активной записи.

Это позволяет избежать добавления ненужного кода на статическом уровне. Я поклонник управления менее для управления (меньше кода для разрыва). Мне нравится использовать рубиновые тонкости, не делая ничего, чтобы заставить его быть "лучше", чем это уже есть.

В настройке monkey-patch вы привязываете код к классу или модулю, который может уйти, и есть сценарии, где он будет терпеть неудачу, пока ваш класс не сможет вызвать acts_as_*.

Одно падение аргумента переносимости - это аргумент тестирования. В этом случае я бы сказал, что вы можете написать свой код для защиты от переносимости или рано сработать с умными предупреждениями о том, что будет и не будет работать при использовании портативно.