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

时间:2016-01-03 01:52:47

标签: ruby-on-rails ruby null-object-pattern

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

我有几个实例方法使用这些属性并执行计算,但我想首先检查它们是否为零,因为我会得到可怕的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?来执行检查有一个糟糕的代码气味,我已经研究了空对象模式,但它似乎没有意义,因为对象存在,但有些属性是{ {1}}

2 个答案:

答案 0 :(得分:1)

在这些情况下,短路通常效果最佳。

在:

if @attribute.present?
  @attribute.do_something
end

的短路:

@attribute && @attribute.do_something

通过短路方法,只要Ruby看到&&运算符的左侧是nil,它就会停止而不会运行右侧

我也会想到为什么特定属性应该被允许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

答案 1 :(得分:1)

我认为真正的问题是number_of_uses可以是nil,(正如您已经发现的那样)引入了大量的复杂性。尽量先消除这个问题。

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

  1. condition ? true : false 总是代码气味。布尔运算符返回布尔值(ish),所以让他们完成工作:

    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. 这个版本无法改进很多,但我个人更喜欢早期returnif块中包含的方法:

    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. 如果truenumber_of_uses位,此方法会返回nil,这有点奇怪,因为我们可以这样简化它:

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

    请注意,我致电remaining_uses.nil?而不是number_of_uses.nil?;当我们从一个人那里得到相同的结果时,没有必要依赖两者。

  5. 进一步改进

    经过进一步考虑后,我认为您可以通过引入另一种方法使此代码的目的更清晰: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返回nil仍困扰着我。事实证明,如果我们返回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