减少两个 类 之间不必要的重复

Reduce unnecessary duplication between two classes

我有两个 classes 负责属性验证:

class NameValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    message = options.fetch(:message, I18n.t('errors.attributes.name.invalid'))
    record.errors[attribute] << message unless NameValidator.valid_name?(value)
  end

  def self.valid_name?(name)
    name =~ /\A[a-z][\w\p{Blank}]+\z/i
  end
end

和第二个

class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    message = options.fetch(:message, I18n.t('errors.attributes.email.invalid'))
    record.errors[attribute] << message unless EmailValidator.valid_email?(value)
  end

  def self.valid_email?(email)
    email =~ /\A.+@.+\..+\z/i
  end
end

他们基本上是一样的。我应该从一个 class 继承它们并使用受保护的实用方法还是什么?

为了清晰起见,将它们分开。这些方法足够小,抽象的混淆会使正在发生的事情更明显,而不是更不明显。

如果您开始拥有 3、4、5、6 或更多类似的验证器,并且此模式开始变得明显,添加抽象可能会使其更易于理解、更改、依赖或删除。

仅当一个 class 显然是另一个的特例时才使用继承。在您的示例中,这两个 class 似乎是相等的。然后,使用 mixin,而不是继承。

您的代码中反对通用化 validate_each 的一个小问题是 NameValidator.valid_name?EmailValidator.valid_email? 的硬编码。您需要在两个 classes 中使用的公共代码中使它们相同。首先,你不需要给valid_name?valid_email?取不同的名字。它们的差异应该通过使用各自的 classes 来吸收。使用通用名称。其次,您不需要对接收器进行硬编码。相反,使用 self.class。但与其使用 class 方法,不如使用实例方法。

module ValidatorModule
  def validate_each(record, attribute, value)
    message = options.fetch(:message, I18n.t("errors.attributes.#{attribute}.invalid"))
    record.errors[attribute] << message unless valid?(value)
  end
end

class NameValidator < ActiveModel::EachValidator
  include ValidatorModule
  def attribute; "name" end
  def valid?(value); value =~ /\A[a-z][\w\p{Blank}]+\z/i end
end

class EmailValidator < ActiveModel::EachValidator
  include ValidatorModule
  def attribute; "email" end
  def valid?(value); value =~ /\A.+@.+\..+\z/i end
end

如果您认为验证总是使用单个正则表达式完成,则可以更进一步:

module ValidatorModule
  def validate_each(record, attribute, value)
    message = options.fetch(:message, I18n.t("errors.attributes.#{attribute}.invalid"))
    record.errors[attribute] << message unless value =~ validation_pattern
  end
end

class NameValidator < ActiveModel::EachValidator
  include ValidatorModule
  def attribute; "name" end
  def validation_pattern; /\A[a-z][\w\p{Blank}]+\z/i end
end

class EmailValidator < ActiveModel::EachValidator
  include ValidatorModule
  def attribute; "email" end
  def validation_pattern; /\A.+@.+\..+\z/i end
end

你可以进一步简化这个

class PatternValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    message = options.fetch(:message) || kind
    record.errors[attribute] << message unless value =~ validation_pattern
  end
end

class NameValidator < PatternValidator
  def validation_pattern; /\A[a-z][\w\p{Blank}]+\z/i end
end

class EmailValidator < PatternValidator
  def validation_pattern; /\A.+@.+\..+\z/i end
end

EachValidator 都有一个#kind 方法,因此它将添加 :name 或 :email 作为失败消息,除非被覆盖。然后您可以让 i18n 按照 rails guide.

中记录的标准级联进行查找