Как добавить в конкретный Ruby-объект какое-нибудь свойство

January 11, 2010 Ruby Ruby on Rails метапрограммирование

Вообще задача такая:

class Book < ActiveRecord::Base
  has_many :ratings
  has_many :tags, :through => :ratings
end
 
class Rating < ActiveRecord::Base
  belongs_to :book
  belongs_to :tag
  validates_presence_of :score
end

Надо возвращать для каждой книжки не только все ее теги, но и сумму оценок, поставленных для каждого тега.

Ущербные решения

Затык был не в том, как закодить саму выборку суммы, а в каком виде возвращать результат. Первая мысль – массивом хешей типа {:tag => tag, :score => score}. Мало того, что это попахивает PHP, так с такими хешами еще и неудобно работать. Потом я хотел сделать total_score методом Tag:

class Tag < ActiveRecord::Base
  def total_score(book)
    return score_computed_some_way
  end
end
 
@book.tags.first.total_score(@book)

Но это тоже бредовая тавтология, как по мне.

Расширение Ruby-объектов на уровне объекта

Итак. Есть такой метод Object#instance_eval – он позволяет запихнуть в объект практически все, что угодно. (Если бы что-то угодно было чем-то более сложным, чем возвращение объекта, можно было бы вынести его в модуль и подключать через Object#extend)

# scores = {tag_id => score, ...}
def inject_scores_into_tags(tags, scores)
  tags.each do |tag|
    tag.instance_eval do
      @total_score = scores[tag.id]
      def total_score
        @total_score
      end
    end
  end
end

После этого у тегов появляется метод total_score.

Buy Me a Coffee at ko-fi.com