Одним из главных 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 ограничений
Условие может быть определено двумя способами, первый способ — через встроенный 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-условия не работают), и для выбранных моделей будет выполнено действие. Объявленные действия будут выполняться не ровно через указанный промежуток времени, а по истечении интервала!
Вместо заключения
Подробнее о подключении в приложение и многом другом можно прочитать на гитхабе (а также заглянуть в исходники!). Жду вопросы, критику и фидбэк в комментарии.
Автор: DmitryTsepelev