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
精彩评论