2011-05-26 2 views
1

Скажем, у меня есть следующий класс:В Ruby, как пометить свойство как грязное?

class Cashier 

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


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

end 

Это для целей обучения только, пожалуйста, игнорировать, как нереально это может быть.

Теперь, в общем случае, total_cash может быть дорогостоящим вызовом для циклического прохождения всех элементов.

Я хочу знать, как я могу вызвать .inject ТОЛЬКО, если переменная @purchases грязная, т. Е. Что-то было изменено.

Как мой класс будет изменен для этого?

+1

Несвязанный комментарий: вы можете сделать 'inject (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 каждый раз, когда связующий вызывается для purchases. Однако это означает, что вам нужно всегда использовать сеттер, даже в пределах вашего класса. Это также означает, что вы будете «скрывать» дорогостоящую операцию внутри метода сеттера. Вы можете решить, какой из них вам больше нравится.

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, что это относительно дорогостоящий вызов, а не простой геттер/сеттер.

+1

отличное предложение по наименованию – Blankman

+0

Вероятно, нет необходимости пересчитывать сумму с нуля каждый раз. Просто обновите его с помощью '@total_cash + = amount'. – hammar

+0

@hammer: Эмм, да ... Я даже не смотрел, что делает код = D. Хорошая точка зрения. –

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 
1

Вы можете сделать это на шаг дальше, чем другие ответы, если хотите. Вместо того, чтобы изменять код, чтобы пересчитать только при необходимости, вы можете указать код, который изменяет код. Все любят метапрограммирование.

Вот код, который берет имя метода, который выполняет потенциально длительный расчет, и список имен методов, которые при вызове invalidate любой предыдущий расчет и записывают код, чтобы обернуть методы и выполнить только вычисление if необходимо, возвращая сохраненное значение, если нет.

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 

Это не могло бы иметь смысл, чтобы сделать что-то вроде этого, если вы просто хотите изменить один метод, но если у вас есть несколько, которые вы хотите изменить, то методы, подобные этому, могут быть весьма полезными.

Смежные вопросы