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

в 6:34, , рубрики: rails, ruby, ruby on rails

Одним из главных 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js