在Ruby中,如何将属性标记为脏?

时间:2011-05-26 18:47:54

标签: ruby

说我有以下课程:

class Cashier

  def purchase(amount)
    (@purchases ||= []) << amount
  end


  def total_cash
    (@purchases || []).inject(0) {|sum,amount| sum + amount}
  end

end

这仅用于学习目的,请忽略这可能是多么不切实际。

现在一般来说,total_cash可能是一个昂贵的调用来遍历所有项目。

我想知道如果@purchases变量是脏的,即只有修改过的内容,我才能调用.inject。

如何修改我的课程呢?

3 个答案:

答案 0 :(得分:1)

最简单的方法是维护另一个变量来指示@purchases是否脏。例如:

class Cashier

  def initialize(*args)
    # init @purchases and @total_cash
    @is_purchases_dirty = false
  end

  def purchase(amount)
    (@purchases ||= []) << amount
    @is_purchases_dirty = true
  end


  def total_cash
    return @total_cash unless @is_purchases_dirty
    @is_purchases_dirty = false
    @total_cash = (@purchases || []).inject(0) {|sum,amount| sum + amount}
    return @total_cash
  end

end

更清晰/更简单的方法可能是每次为@total_cash调用setter时计算purchases。但是,这意味着您需要始终使用setter,即使在您的班级中也是如此。这也意味着您将在setter方法中“隐藏”昂贵的操作。你可以决定哪一个更好。

class Cashier

  def purchase(amount)
    (@purchases ||= []) << amount
    @total_cash = (@purchases || []).inject(0) {|sum,amount| sum + amount}
  end


  def total_cash
    @total_cash
  end

end

我还建议您不要使用命名方案进行昂贵的操作。我会将total_cash重命名为calc_total_cash,以便告诉用户您的API,这是一个相对昂贵的调用,而不是简单的getter / setter。

答案 1 :(得分:1)

如果您愿意,可以比其他答案更进一步。您可以编写更改代码的代码,而不是将代码更改为仅在必要时重新计算。每个人都喜欢一点元编程。

这里有一些代码,它采用执行潜在长计算的方法的名称,以及调用时无效的任何先前计算的方法名称列表,并编写代码以包装方法,并仅在必要时执行计算,如果没有,则返回存储的值。

module ExpensiveCalculation

  def recalc_only_if_necessary(meth, *mutators)

    aliased_method_name = "__#{meth.object_id}__"
    value = "@__#{meth.object_id}_value__"
    dirty_flag = "@__#{meth.object_id}_dirty__"

    module_eval <<-EOE
      alias_method :#{aliased_method_name}, :#{meth}
      private :#{aliased_method_name}

      def #{meth}(*args, &blk)
        #{dirty_flag} = true unless defined? #{dirty_flag}
        return #{value} unless #{dirty_flag}
        #{value} = #{aliased_method_name}(*args, &blk)
        #{dirty_flag} = false
        #{value}
      end        
    EOE

    mutators.each do |mutator|
      aliased_mutator = "__#{meth.object_id}_#{mutator.object_id}__"

      module_eval <<-EOE
        alias_method :#{aliased_mutator}, :#{mutator}
        private :#{aliased_mutator}

        def #{mutator}(*args, &blk)
          #{dirty_flag} = true
          #{aliased_mutator}(*args, &blk)
        end
      EOE
    end
  end

  # this hook is used to make the new method
  # private to the extended class.
  def self.extend_object(obj)
    super
    obj.private_class_method :recalc_only_if_necessary
  end

end

通过在类定义中使用它,您可以轻松地包装一个或多个方法,而无需更改现有代码:

class Cashier
  extend ExpensiveCalculation

  def purchase(amount)
    (@purchases ||= []) << amount
  end

  def total_cash
    (@purchases || []).inject(0) {|sum,amount| sum + amount}
  end

  recalc_only_if_necessary :total_cash, :purchase
end

如果您只想更改一个方法,那么做这样的事情可能没有意义,但是如果您想要改变一些方法,那么像这样的技术可能非常有用。

答案 2 :(得分:0)

在最简单的情况下,您可以为要标记为脏的事物定义实例变量。修改变量时(在true方法中)将其设置为purchase

检查total_cash中的值;如果是这样,请使用总计的缓存版本。否则,计算新值并将其存储在缓存中。

class Cashier

  def purchase(amount)
    @purchases_dirty = true
    (@purchases ||= []) << amount
  end


  def total_cash
    @total_cash = (@purchases || []).inject(0) do |sum,amount|
      sum + amount
    end if (@purchases_dirty || @total_cash.nil?)
    @purchases_dirty = false
    @total_cash
  end

end