在 Parent 的验证中使用 Child 的常量

Using a Child's constant within a Parent's Validation

使用下面的代码我可以访问 child 的常量 (ADDRESS_FIELDS) 在 initialize 方法中没有问题(通过使用 self.class::ADDRESS_FIELDS)但是我无法在验证中访问它(得到 NameError: uninitialized constant Class::ADDRESS_FIELDS)。关于如何在 parent 验证中使用 child 的常量有什么想法吗?还有 PaymentType 的其他 children,他们自己的值为 ADDRESS_FIELDS

class PaymentType < ActiveRecord::Base
  attr_accessible :address

  validates :address, hash_key: { presence: self.class::ADDRESS_FIELDS }

  def initialize(attributes = {}, options = {})
    super
    return self if address.present?

    address = {}
    self.class::ADDRESS_FIELDS.each do |field|
      address[field] = nil
    end
    self.address = address
  end
end

class WireTransfer < PaymentType
  ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
end

到处都用全名来指代它:

WireTransfer::ADDRESS_FIELDS

或者在子模型中,您可以简单地使用:

ADDRESS_FIELDS

无需预先添加 self.class

昨天很高兴 chatting 和你在一起。回顾一下,您将 validates 调用放入 PaymentType 的动机是让您的代码变干(因为它在 PaymentType 的所有 children 中都是相同的)。

问题是 Ruby 在加载 WireTransfer 之前加载 PaymentType(我相信是由于继承)所以 validates 找不到 ADDRESS_FIELDS(因为它是在尚未加载的 WireTransfer 上定义的)。这是下面 RSpec 测试中的第一个测试。

rspec 'spec/stack_overflow/child_constant_parent_validation_spec.rb' -fd

Using a Child's Constant within a Parent's Validation
  when 'validates' in parent
    raises error

现在,您可以在每个 child 中放入 validates。但是,这有点糟糕,因为你必须在每个 child 中定义它——但它在所有 children 中都是相同的。所以,你并不像你想的那样干。这是下面的第二个测试。

rspec 'spec/stack_overflow/child_constant_parent_validation_spec.rb' -fd

Using a Child's Constant within a Parent's Validation
  when 'validates' in parent
    raises error
  when 'validates' in child
    doesn't raise an error
    has the correct class methods
    has the correct instance methods
    kinda sucks because 'validates' has to be defined in every child.

那么,你注定要湿漉漉的吗?不必要。你可以把你的 validates 放在一个模块中,这样你就可以定义一次并在任何地方使用它。然后,您可以将该模块包含在您的 children 类 中。诀窍是 (1) 使用 included 挂钩并访问 base::ADDRESS_FIELDS,以及 (2) 在 [=] 中设置 ADDRESS_FIELDS 之后确保 include 模块45=]。这是下面的第三个测试。

rspec 'spec/stack_overflow/child_constant_parent_validation_spec.rb' -fd

Using a Child's Constant within a Parent's Validation
  when 'validates' in parent
    raises error
  when 'validates' in child
    doesn't raise an error
    has the correct class methods
    has the correct instance methods
    kinda sucks because 'validates' has to be defined in every child.
  when 'validates' in module
    doesn't raise an error
    has the correct class methods
    has the correct instance methods
    is a little better because you can define 'validates' once and use in all children

Finished in 0.00811 seconds (files took 0.1319 seconds to load)
9 examples, 0 failures

当然,您仍然需要记住在每个 child 中包含该模块,但这应该不会太糟糕。比到处定义 validates 更好。

完成所有操作后,您的 类 可能看起来像:

class PaymentType
  class << self
    def a_useful_class_method_from_payment_base; end
  end
  def a_useful_instance_method_from_payment_base; end
end

module PaymentTypeValidations
  def self.included(base)
    validates :address, hash_key: { presence: base::ADDRESS_FIELDS }
  end
end

class WireTransfer < PaymentType
  ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
  include PaymentTypeValidations
end

class Bitcoin < PaymentType
  ADDRESS_FIELDS = %i(wallet_address)
  include PaymentTypeValidations
end

我把整个 RSpec 测试放在下面,以防你想 运行 自己测试。

RSpec.describe "Using a Child's Constant within a Parent's Validation " do

  before(:all) do

    module Validations
      def validates(field, options={}) 
        define_method("valid?") do
        end
        define_method("valid_#{field}?") do
        end
      end
    end

    module PaymentType
      class Base
        extend Validations
        class << self
          def a_useful_class_method_from_payment_base; end
        end
        def a_useful_instance_method_from_payment_base; end
      end
    end

    module WireTransfer
    end

  end

  context "when 'validates' in parent" do
    it "raises error" do

      expect{

        class PaymentType::WithValidates < PaymentType::Base
          validates :address, hash_key: { presence: self::ADDRESS_FIELDS }
        end

        class WireTransfer::Base < PaymentType::WithValidation
          ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
        end

      }.to raise_error(NameError)

    end
  end

  context "when 'validates' in child" do
    it "doesn't raise an error" do

      expect{

        class PaymentType::WithoutValidates < PaymentType::Base
        end

        class WireTransfer::WithValidates < PaymentType::WithoutValidates
          ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
          validates :address, hash_key: { presence: self::ADDRESS_FIELDS }
        end

      }.to_not raise_error
    end
    it "has the correct class methods" do
      expect(WireTransfer::WithValidates).to respond_to("a_useful_class_method_from_payment_base")
    end
    it "has the correct instance methods" do
      wire_transfer = WireTransfer::WithValidates.new
      ["valid?","valid_address?","a_useful_instance_method_from_payment_base"].each do |method|
        expect(wire_transfer).to respond_to(method)
      end
    end
    it "kinda sucks because 'validates' has to be defined in every child." do
      module Bitcoin
        class Base < PaymentType::WithoutValidates
        end
      end
      bitcoin = Bitcoin::Base.new
      ["valid?","valid_address?"].each do |method|
        expect(bitcoin).to_not respond_to(method)
      end
    end
  end
  
  context "when 'validates' in module" do
    it "doesn't raise an error" do
      expect{

        module PaymentTypeValidations
          extend Validations
          def self.included(base)
            validates :address, hash_key: { presence: base::ADDRESS_FIELDS }
          end
        end

        class WireTransfer::IncludingValidationsModule < PaymentType::WithoutValidates
          ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
          include PaymentTypeValidations
        end

      }.to_not raise_error

    end

    it "has the correct class methods" do
      expect(WireTransfer::IncludingValidationsModule).to respond_to("a_useful_class_method_from_payment_base")
    end

    it "has the correct instance methods" do
      wire_transfer = WireTransfer::IncludingValidationsModule.new
      ["valid?","valid_address?","a_useful_instance_method_from_payment_base"].each do |method|
        expect(wire_transfer).to respond_to(method)
      end
    end

    it "is a little better because you can define 'validates' once and use in all children" do
      class Bitcoin::IncludingValidationsModule < PaymentType::WithoutValidates
        ADDRESS_FIELDS = %i(wallet_address)
        include PaymentTypeValidations
      end

      bitcoin = Bitcoin::IncludingValidationsModule.new
      ["valid?","valid_address?","a_useful_instance_method_from_payment_base"].each do |method|
        expect(bitcoin).to respond_to(method)
      end

    end


  end

end