Rails 实例方法取决于所设置的属性

Rails instance methods conditional depending on attributes being set

我有一个模型,它有几个属性,在保存模型时这些属性是可选的。

我有几个使用这些属性并执行计算的实例方法,但我想先检查它们是否不为零,因为我会得到可怕的 no method error nil nil class

除了用 .present? 乱写我的代码之外,还有更好的方法吗?

编辑: 到目前为止,这是我的代码

def is_valid?
   (has_expired? === false and has_uses_remaining?) ? true : false
end

def has_expired?
   expiry_date.present? ? expiry_date.past? : false
end

def remaining_uses
  if number_of_uses.present?
    number_of_uses - uses_count
  end
end

def has_uses_remaining?
  number_of_uses.present? ? (remaining_uses > 0) : true
end

我想加入 .present? 来执行检查有不好的代码味道,我已经研究了空对象模式,但它在这里似乎没有意义,因为对象存在但一些属性是 nil

Short-circuiting 通常在这些情况下效果最好。

之前:

if @attribute.present?
  @attribute.do_something
end

Short-circuiting:

@attribute && @attribute.do_something

使用short-circuiting方法,一旦Ruby看到&&运算符的左侧是nil,它就会停止而不是[=38] =]右边

我也会认真思考为什么应该允许某个特定属性 nil(正如乔丹所问)。如果你能想办法避免这种情况,那可能会更好。

假设您确实希望 number_of_users 能够成为 nil,您可以像这样重写 has_uses_remaining?

def has_uses_remaining?
  !number_of_uses || remaining_uses > 0
end

-旁注:您的第一种方法可以简化为:

def is_valid?
   !has_expired? && has_uses_remaining?
end

我认为这里真正的问题是 number_of_uses 可以是 nil,这(如您所见)引入了大量的复杂性。先尝试解决这个问题。

如果由于某种原因你不能这样做,你的每个方法都可以改进:

  1. condition ? true : false 总是 代码味道。布尔运算符 return 布尔值,所以让它们完成它们的工作:

    def is_valid?
      !has_expired? && has_uses_remaining?
    end
    
  2. 我个人认为使用 Rails' Object#try 通常是一种代码味道,但这里非常适合:

    def has_expired?
      expiry_date.try(:past?)
    end
    

    或者:

    def has_expired?
      expiry_date.present? && expiry_date.past?
    end
    
  3. 这个不能改进很多,但我个人更喜欢早期的 return 而不是包装在 if 块中的方法:

    def remaining_uses
      return if number_of_uses.nil?
      number_of_uses - uses_count
    end
    

    你也可以 number_of_uses && number_of_uses - uses_count (甚至 number_of_uses.try(:-, uses_count) 但我认为这样更清楚。

  4. 这个方法 returns true if number_of_uses is nil 有点奇怪,因为它确实如此,我们可以像这样简化它:

    def has_uses_remaining?
      remaining_uses.nil? || remaining_uses > 0
    end
    

    注意我调用的是remaining_uses.nil?而不是number_of_uses.nil?;当我们可以从一个获得相同的结果时,就没有必要依赖两者。

进一步改进

经过进一步考虑,我认为您可以通过引入另一种方法使这段代码的意图更加清晰:has_unlimited_uses?:

def has_unlimited_uses?
  number_of_uses.nil?
end

def is_valid?
  !has_expired? &&
    has_unlimited_uses? || has_uses_remaining?
end

def remaining_uses
  return if has_unlimited_uses?
  number_of_uses - uses_count
end

def has_uses_remaining?
  has_unlimited_uses? || remaining_uses > 0
end

这样就不会对您要检查的内容有任何歧义。这将使代码对下一个阅读它的人(或六个月后的你)更具可读性,并使跟踪错误更容易。

不过,remaining_uses returns nil 仍然困扰着我。事实证明,如果我们 return Float::INFINITYhas_uses_remaining? 变成一个简单的比较:

def remaining_uses
  return Float::INFINITY if has_unlimited_uses?
  number_of_uses - uses_count
end

def has_uses_remaining?
  remaining_uses > 0
end