Расширение ассоциаций в Rails

Опубликовано 12.10.2009

Не прошло и полугода с моей последней более-менее пристойной заметки :) Надо сказать, последние несколько месяцев выдались настолько насыщенные, что даже не было желания что-либо такое интересное написать – все мозги уходили на разработку действительно классных вещей. Однако сегодня, наконец, пришло понимание, что дальше так продолжаться не может – пора делиться мыслями, бешено роящимися в голове.

Давайте немного поговорим об ассоциациях. Если вы программируете на Rails дольше 15 минут, то уже наверняка знаете, что рельсы поддерживают несколько типов ассоциаций – belongs_to, has_one, has_many и has_and_belongs_to_many. Каждый тип ассоциаций решает определенную задачу, у каждого свой API. Но есть у них одна общая замечательная особенность – ассоциации можно расширять.

Допустим, у нас есть проект “Доска объявлений”, где пользователь может опубликовать до 5 объявлений каждое стоимостью 10 долларов. Давайте взглянем на модель пользователя, содержащую несколько методов для проверки возможности публикации объявлений:

class User < ActiveRecord::Base
  has_many :classifieds

  def can_publish_classified?
    has_free_classified_slots? and enough_money_to_publish_classified?
  end

  def has_free_classified_slots?
    self.classifieds.size <= 5
  end

  def enough_money_to_publish_classified?
    self.balance >= 10
  end
end

Вот так выглядят вызовы методов проверок:

@user.can_publish_classified?
@user.has_free_classified_slots?
@user.enough_money_to_publish_classified?

Обратите внимание на постоянно повторяющееся слово classified в названиях методов. Было бы гораздо удобнее, если бы методы вызывались в контексте того, к чему они непосредственно относятся, а именно – в контексте ассоциации classifieds. Именно для таких случаев в Rails реализована возможность расширения ассоциаций, а точнее – прокси-классов, которые эти ассоциации обслуживают. Делается это следующим образом:

class User < ActiveRecord::Base
  has_many :classifieds do
    def can_publish?
      free_slots? and enough_money_to_publish?
    end

    def free_slots?
      self.size <= 5
    end

    def enough_money_to_publish?
      proxy_owner.balance >= 10
    end
  end
end

Теперь проверки возможности публикации объявлений выглядят у нас так:

@user.classifieds.can_publish?
@user.classifieds.free_slots?
@user.classifieds.enough_money_to_publish?

Существенно лучше, не так ли? Обратите внимание на метод proxy_owner, который вызывается в методе enough_money_to_publish?. Его описание, а так же описание других полезных методов прокси-класса ассоциации можно найти в документации (см. Association extensions):

  • proxy_owner – объект, частью которого является данная ассоциация (в нашем случае экземпляр класса User)
  • proxy_reflection – объект класса ActiveRecord::Reflection::AssociationReflection, хранящий в себе сведения об ассоциации
  • proxy_target – объект или коллекция объектов, которые загружаются ассоциацией (в нашем случае это коллекция объявлений, привязанных к пользователю); загрузка этих данных выполняется вызовом метода load_target

Однако давайте допустим, что авторами объявлений у нас могут стать не только пользователи, но еще и компании (модель Company). Есть два пути добавления методов в ассоциацию второй модели – copy-paste и вынос методов в отдельный модуль. Я думаю, нет сомнений, что второй путь более правильный.

Выносим код расширения в отдельный модуль:

module ClassifiedExtension
  def can_publish?
    free_slots? and enough_money_to_publish?
  end

  def free_slots?
    self.size <= 5
  end

  def enough_money_to_publish?
    proxy_owner.balance >= 10
  end
end

А затем расширяем им ассоциации:

class User < ActiveRecord::Base
  has_many :classifieds, :extend => ClassifiedExtension
end

class Company < ActiveRecord::Base
  has_many :classifieds, :extend => ClassifiedExtension
end

Как видите, расширять ассоциации совсем не сложно. Однако, как ни странно, далеко не все разработчики, даже опытные, используют эту возможность. А зря :)

Подпишись и читай
Самые продвинутые ruby-программеры уже читают Rail0rz в формате RSS. Присоединяйся!
Комментарии
  1. icerock12.10.2009

    Интересно, спасибо за статью :) Добавим элегантности нашим ‘relationships’.

  2. mikhailov13.10.2009

    сколько не читал статей и api по расширению ассоциаций, не возникало желания воспользоваться этой возможностью. api, например, содержит слишком общий пример использования. спасибо, Декарт, что дал осознать, что поинт здесь - контекст вызова методов. вопрос - мы можем распширять только методы экземпляра, или также и статические методы, которые можно вынести в модуль и примешать с помощью extend?

  3. DEkart13.10.2009

    mikhailov, не очень понял что ты имеешь в виду.

  4. rwz13.10.2009

    Очень круто!

  5. mikhailov13.10.2009

    перефразирую, можно ли расширить конкретную ассоциацию методом класса?

  6. DEkart14.10.2009

    mikhailov, ну в принципе это можно сделать, добавив в модуль метод self.included(base), и внутри него сделав base.extend(MyClassMethods)

    Но вопрос - зачем?

  7. buriy27.10.2009

    кстати, не enought, а enough ;) я так, мимо проходил)

  8. DEkart28.10.2009

    buriy, ага, спасибо, поправил :)