Triggerable — событийно-ориентированная логика для ActiveRecord моделей

Одним из главных Rails-трендов в данный момент является переосмысление роли ActiveRecord-классов в приложении: отныне модели должны стать классами, отвечающими за работу с БД, а не солянкой из запросов, ассоциаций, валидаций, методов предметной области и методов представления. Несмотря на огромные модели, часть логики предметной области все равно переезжает в другие части приложения, и это сильно усложняет её понимание. Во многих приложениях много действий совершается при возникновении событий, при этом используются средства ActiveRecord: Callbacks. Данный gem — попытка переосмыслить описание бизнес-правил для ActiveRecord-моделей.Итак, triggerable представляет собой gem для описания событийно-ориентированных бизнес-правил высокого уровня. Правила могут быть объявлены как в контексте класса-модели, так и вынесены из нее в отдельный файл. В данный момент библиотека включает реализацию двух типов правил: триггеры и автомации. Триггер — это правило, включающее в себя событие, условие выполнения и действие. К примеру, пусть нам необходимо отпавлять новым пользователям SMS после регистрации, но при условии, что пользователь согласился на получение сообщений от нас. Объявим простой триггер: User.trigger name: 'SMS to user', on: : after_create, if: { receives_sms: true } do SmsGateway.send_welcome_sms (phone_number) end Как это работает: в модель добавляется специальный callback, который инициирует выполнение всех объявленных триггеров при выполнении условия. Действие (блок do) будет выполнено в контексте модели. Так как данное правило реализованно на основе ActiveRecord: Callbacks используется тот же список событий (before_save, after_create и т.д.). В объявление правила передается необязательный атрибут name, он может быть использован, к примеру, для логгирования действий правил. Условие может быть определено двумя способами, первый способ — через встроенный DSL, в этом случае значением if является хэш. Чтобы наложить ограничение на поле необходимо использовать его название в качестве ключа, а значением будет являться хэш с условиями. В приведенном выше примере используется короткая форма сравнения — в полной форме можно использовать условие { receives_sms: { is: true } }. В данный момент доступны следующие простые условия: Тип Полная форма Краткая форма Значение { field: { is: : value } } { field: : value } Принадлежность { status: { in: [: open, : accepted] } } { status: [: open, : accepted] } Отрицание { field: { is_not: : value } Больше { field: { greater_then: : value } Меньше { field: { less_then: : value } Существование { field: { exists: true } Кроме того, доступно комбинирование условий через and и or: { and: [{ field1: : value1 }, { field2: : value2 }] } { or: [{ field1: : value1 }, { field2: : value2 }] } Если необходимо использовать проверку ассоциаций (в данный момент не поддерживаемых DSL) или какой-либо другой сложный случай, то можно воспользоваться вторым способом — lambda-условием. В этом случае значением if является блок, при этом внутри блока будет сохраняться контекст модели, например: User.trigger on: : after_create, if: { receives_sms? && payments.count > 0 } do send_welcome_sms end

Ранее мы уже объявляли действия, используя блок do, однако в случаях, когда одинаковые действия выполняются объектов разных классов можно избежать дублирования кода, используя свой собственный класс действия. Для этого необходимо унаследоваться от класса Triggerable: Actions: Action и реализовать единственный метод def run_for!(object, rule_name), в котором первым аргементом будет объект, на котором запущен триггер, а вторым — имя правила (переданный в атрибуте name, см. выше).Вернемся к примеру с отправкой SMS. Допустим в системе могут быть зарегистрированы клиенты (класс Customer) которые также должны получать SMS после регистрации. Создаем новый класс действия и триггеры: class SendWelcomeSms < Triggerable::Actions::Action def run_for! object, trigger_name SmsGateway.send_welcome_sms(object.phone_number) end end

User.trigger on: : after_create, if: { receives_sms: true }, do: : send_welcome_sms

Customer.trigger on: : after_create, if: { and: [{ receives_sms: true }, { active: true}] }, do: : send_welcome_sms

Автомация — выполнение отложенного действия при выполнении условий. К примеру, пусть нам требуется отправлять пользователям сообщение не сразу, а по истечении 24 часов. Автомация будет выглядеть следующим образом: User.automation name: 'SMS to user', if: { created_at: { after: 24.hours }, receives_sms: true } do: : send_welcome_sms Отличия от объявления триггера:1. Не указывается событие (on)2. В блоке условия указывается время выполнения (before или after)3. lambda-условия запрещеныКак это работает: для работы автомаций необходимо подключить какой-либо движок для организации плановых задач (к примеру whenever) и обеспечить запуск движка автомаций: Triggerable: Engine.run_automations (interval), где interval — промежуток времени между запусками задачи. При запуске для каждой автомации будет выполняться запрос к БД, построенный на основе объявленных условий (поэтому lambda-условия не работают), и для выбранных моделей будет выполнено действие. Объявленные действия будут выполняться не ровно через указанный промежуток времени, а по истечении интервала!

Подробнее о подключении в приложение и многом другом можно прочитать на гитхабе (а также заглянуть в исходники!). Жду вопросы, критику и фидбэк в комментарии.

© Habrahabr.ru