开发者

In Ruby, how to mark a property as being dirty?

Say I have the following class:

class Cashier

  def purchase(amount)
    (@purchases |开发者_如何学JAVA|= []) << amount
  end


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

end

This is for learning purposes only, please ignore how unrealistic this may be.

Now in general, the total_cash could be an expensive call to loop through all the items.

I want to know how I can call .inject ONLY if the @purchases variable is dirty i.e. there was something modified.

How would my class be modified to do this?


The simplest approach would be to maintain another variable to indicate whether or not @purchases is dirty. For example:

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

A cleaner/simpler approach may be to calculate @total_cash each time the setter is called for purchases. However, this means that you need to always use the setter, even within your class. It also means that you will be "hiding" an expensive operation inside of a setter method. You can decide which one you like better.

class Cashier

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


  def total_cash
    @total_cash
  end

end

I would also recommend against your naming scheme for an expensive operation. I would rename total_cash to something like calc_total_cash in order to tell users of your API that this is a relatively expensive call as opposed to a simple getter/setter.


You can take this a step further than the other answers if you wanted. Rather than changing your code to only recalculate when necessary, you can write the code that changes your code. Everybody loves a bit of metaprogramming.

Here's some code that takes the name of a method that performs a potentially long calculation, and a list of names of methods that when called invalidate any previous calculation, and writes the code to wrap the methods and only perform the calculation if necessary, returning the stored value if not.

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

By making this available inside your class definition, you can wrap one or many methods easily without changing your existing code:

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

It might not make sense to do something like this if you just want to change one method, but if you have several that you want to change some way techniques like this can be pretty useful.


In the simplest case, you could define an instance variable for the thing you want to mark as dirty. Set it to true when the variable is modified (in your purchase method).

Check for the value in total_cash; if so, use a cached version of the total. Otherwise, compute the new value and store it in the cache.

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
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜