Rails / Ruby,混合模块和剩余DRY

时间:2015-07-23 14:31:09

标签: ruby-on-rails ruby

我正在尝试为我的rails应用程序编写更多的模块化代码,因此已经开始在类中包含模块。我对它们的功能有基本的了解,但我发现在保持DRY的同时很难让它们保持灵活性。

这是一个当前的例子。

我有一个名为Contactable的模块。它有两个非常基本的功能。

  1. 确保数据库中存在正确的联系人列。
  2. 验证某些字段。
  3. 这是

    module Contactable
      extend ActiveSupport::Concern
      ERROR = 'please ensure necessary fields are in place'
    
      included do
        REQUIRED_DATABASE_FIELDS.map { |rdf| raise "#{rdf} not included. #{ERROR}" unless column_names.include?(rdf)}
        REQUIRED_INPUT_FIELDS.map { |rif| validates rif.to_sym, presence: true}
      end
    end
    

    我希望可以联系其他三个模块(可电话,可电子邮件和可寻址),其中包含要求的列数组和要验证的字段。我现在正在研究的是“可寻址的”

    module Addressable 
      extend ActiveSupport::Concern
      ERROR = 'please ensure necessary fields are in place'
    
      REQUIRED_DATABASE_FIELDS = %w{address1 
                                    address2 
                                    address3 
                                    town 
                                    county 
                                    country 
                                    postcode}
    
      REQUIRED_INPUT_FIELDS = %w{address1 postcode}
    
      included do
        REQUIRED_DATABASE_FIELDS.map { |rdf| raise "#{rdf} not included. #{ERROR}" unless column_names.include?(rdf)}
        REQUIRED_INPUT_FIELDS.map { |rif| validates rif.to_sym, presence: true}
      end
    end
    

    显然这里有重复。但是,如果我将此模块包含在可联系中,我可以避免重复某些操作,但这意味着Contactable将始终包含Phoneable和Emailable。在某些情况下,我可能不想验证或要求这些特征。有没有办法可以实现这种灵活性?

2 个答案:

答案 0 :(得分:1)

您可以这样做:

添加 /app/models/concerns/fields_validator.rb

module FieldsValidator
  extend ActiveSupport::Concern

  class_methods do
    def validate_required_attributes
      required_attributes.each do |a|
        puts "adds validation for #{a}"
        validates(a.to_sym, presence: true)
      end
    end

    def load_required_attributes(*_attrs)
      puts "loading attrs: #{_attrs.to_s}"
      @required_attributes ||=[]
      @required_attributes += _attrs
      @required_attributes.uniq!
    end

    def required_attributes
      @required_attributes
    end
  end
end

添加 /app/models/concerns/contact.rb

module Contact
  extend ActiveSupport::Concern
  include FieldsValidator

  included do
    puts "include contact..."
    load_required_attributes(:product_details, :observations, :offer_details)
  end
end

添加 /app/models/concerns/address.rb

module Address
  extend ActiveSupport::Concern
  include FieldsValidator

  included do
    puts "include address..."
    load_required_attributes(:sku, :amount, :observations)
  end
end

在模型中......

class Promotion < ActiveRecord::Base
  include Address
  include Contact

  validate_required_attributes
end

输出:

include address...
loading attrs: [:sku, :amount, :observations]
include contact...
loading attrs: [:product_details, :observations, :offer_details]
adds validation for sku
adds validation for amount
adds validation for observations
adds validation for product_details
adds validation for offer_details

要检查这是否有效......

Promotion.new.save!
"ActiveRecord::RecordInvalid: Validation failed: Sku can't be blank, Amount can't be blank, Observations can't be blank, Product details can't be blank, Offer details can't be blank"

<强>考虑:

  • 将您的模块保存在自定义命名空间中。您将遇到现有Addressable模块的问题。例如:

    module MyApp
      module Addressable
      # code...
      end
    end
    
    class Promotion < ActiveRecord::Base
      include MyApp::Addressable
    
      validate_required_attributes
    end
    
  • 您需要先加载所有属性,然后应用验证。如果你不这样做,你可以在模块共享属性时重复验证。

  • 共享逻辑进入FieldsValidator模块

答案 1 :(得分:0)

你应该在这里使用单元测试。通过从模型(或它们包含的模块)检查数据库模式,您并没有真正实现任何目标。如果列不存在,则应用程序将抛出NoMethodError或数据库驱动程序错误。

实际上有更好的单元测试可以覆盖你的模型并确保它们按预期工作。

require 'rails_helper'

describe User
  # Tests the presence of the database column indirectly. 
  it { should respond_to :email }

  # Explicit test - there a very few good reasons to actually do this.
  it "should have the email column" do
    expect(User.column_names).to have_key :email 
  end
end

如果您使用的是RSpec,则可以使用共享示例来减少规格中的重复次数。

# support/example_groups/addressable.rb
require 'spec_helper'
RSpec.shared_examples_for "an addressable" do
  it { should respond_to :address1 }
  it { should respond_to :address2 }
  it { should respond_to :address3 } 
  it { should respond_to :county } 
  it { should respond_to :postcode } 
  # ...
end
require 'rails_helper'
require 'support/example_groups/addressable'

describe User
  it_should_behave_like "an addressable"
end

有关如何使用test_unit / minitest实现相同功能的示例,请参阅How do i get RSpec's shared examples like behavior in Ruby Test::Unit?