Как использовать переменные класса из методов класса и экземпляра, которые смешиваются через модуль

Я хочу иметь возможность сделать параметр, переданный моему методу класса (поддающийся проверке), доступным для методов экземпляра. Я смешиваю методы класса и экземпляра, используя модуль.

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

неинициализированная переменная класса @@auditable_only_once в Auditable

class Document
  include Auditable
  auditable :only_once => true
end

# The mixin
module Auditable
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def auditable(options = {})

      options[:only_once] ||= false

      class_eval do
        # SET THE OPTION HERE!!
        @@auditable_only_once = options[:only_once]
      end
      end
    end

    private

    def audit(action)
      # AND READ IT BACK LATER HERE
      return if @@auditable_only_once && self.audit_item
      AuditItem.create(:auditable => self, :tag => "#{self.class.to_s}_#{action}".downcase, :user => self.student)
    end    
  end

Я вырезал часть кода, чтобы его было легче читать, полный код находится здесь: https://gist.github.com/1004399 (EDIT: Gist теперь включает решение)


person Kris    schedule 02.06.2011    source источник
comment
В версии на github есть только один @ перед auditable_only_once при назначении (строка 16), но вы исправили это в коде здесь. Вы тестировали код с этим исправлением? Это все еще не работает?   -  person Jonathan    schedule 02.06.2011
comment
Спасибо, что заметили это, я на самом деле попробовал одиночные и двойные @, поэтому, должно быть, скопировал более новую версию кода в Gist. Теперь исправлено.   -  person Kris    schedule 03.06.2011


Ответы (1)


Использование переменных экземпляра класса @@ нерегулярно, и количество случаев, когда они строго необходимы, чрезвычайно редко. В большинстве случаев кажется, что они просто вызывают проблемы или путаницу. Как правило, вы можете без проблем использовать обычные переменные экземпляра в контексте класса.

Возможно, вы захотите использовать другой шаблон для такого рода вещей. Если у вас есть mattr_accessor, который предоставляется ActiveSupport, вы можете использовать его вместо этой переменной, или вы всегда можете написать свой собственный эквивалент в компоненте ClassMethods.

Один из подходов, который я использовал, состоит в том, чтобы разбить ваше расширение на два модуля: хук и реализацию. Хук только добавляет методы к базовому классу, которые можно использовать для добавления остальных методов, если это необходимо, но в остальном не загрязняет пространство имен:

module ClassExtender
  def self.included(base)
    base.send(:extend, self)
  end

  def engage(options = { })
    extend ClassExtenderMethods::ClassMethods
    include ClassExtenderMethods::InstanceMethods

    self.class_extender_options.merge!(options)
  end
end

Этот метод engage можно назвать как угодно, так как в вашем примере это auditable.

Затем вы создаете модуль-контейнер для методов класса и экземпляра, которые расширение добавляет при выполнении:

module ClassExtenderMethods
  module ClassMethods
    def class_extender_options
      @class_extender_options ||= {
        :default_false => false
      }
    end
  end

  module InstanceMethods
    def instance_method_example
      :example
    end
  end
end

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

Вы можете определить простой пример:

class Foo
  include ClassExtender

  engage(:true => true)
end

Затем проверьте, правильно ли он работает:

Foo.class_extender_options
# => {:default_false=>false, :true=>true}

foo = Foo.new
foo.instance_method_example
# => :example
person tadman    schedule 02.06.2011
comment
Обратите внимание, что я использовал cattr_accessor (не mattr_accessor) для создания атрибута класса (я использую Rails, так что это работает) и для доступа к нему из метода экземпляра, который я использовал self.class.only_once. - person Kris; 14.06.2011