Не прошло и полугода с моей последней более-менее пристойной заметки :) Надо сказать, последние несколько месяцев выдались настолько насыщенные, что даже не было желания что-либо такое интересное написать – все мозги уходили на разработку действительно классных вещей. Однако сегодня, наконец, пришло понимание, что дальше так продолжаться не может – пора делиться мыслями, бешено роящимися в голове.
Давайте немного поговорим об ассоциациях. Если вы программируете на 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
Как видите, расширять ассоциации совсем не сложно. Однако, как ни странно, далеко не все разработчики, даже опытные, используют эту возможность. А зря :)

Интересно, спасибо за статью :) Добавим элегантности нашим ‘relationships’.
сколько не читал статей и api по расширению ассоциаций, не возникало желания воспользоваться этой возможностью. api, например, содержит слишком общий пример использования. спасибо, Декарт, что дал осознать, что поинт здесь - контекст вызова методов. вопрос - мы можем распширять только методы экземпляра, или также и статические методы, которые можно вынести в модуль и примешать с помощью extend?
mikhailov, не очень понял что ты имеешь в виду.
Очень круто!
перефразирую, можно ли расширить конкретную ассоциацию методом класса?
mikhailov, ну в принципе это можно сделать, добавив в модуль метод self.included(base), и внутри него сделав base.extend(MyClassMethods)
Но вопрос - зачем?
кстати, не enought, а enough ;) я так, мимо проходил)
buriy, ага, спасибо, поправил :)