[Из песочницы] Альтернатива callback-ам

Давайте предположим, что нужно сделать рельсовое приложение, которое позволяет создавать ордер, в зависимости от входных данных ордера создавать один или несколько сервисов, резервировать какие-нибудь ресурсы под эти сервисы.

В ходе обработки ордер меняет свое состояние от нового до выполненного, при этом создаются несколько сервисов (в зависимости от данных) и они должны быть запущенны и работать к концу обработки заказа. Простой пример — вы оформляете себе симку для сотового. К этой симке «подключаются» сервисы голосовой связи, СМС-ок и ММС-ок, мобильного интернета (у которого свои тарифы), автоответчик, определитель номера и т.д. К окончанию обработки вашего договора (заказа) все эти сервисы должны быть запущены и работать. Далее вы можете заключить доп. договор и переключиться на др. тариф мобильного интернета и т.д. Это просто пример логики, на который я буду ссылаться для наглядности.

Абсолютное большинство программистов начнет делать такое приложение на колбэках или тригерах. Создан новый ордер — ставим ему состояние new — и вешаем колбэк который начинает создавать сервисы и т.д. Далее я постараюсь объяснить, почему это абсолютное зло.

Колбэки не контролируются


Пока в вашем приложении 10–15 колбэков — вы еще можете их как-то контролировать. Как только моделей в приложении становится больше — колличество колбэков растет. Один колбэк меняет данные, из-за этого срабатывает второй колбэк, меняет другие данные — срабатывает третий и т.д. В результате восстановить всю цепочку и понять что и почему возникло становится не просто. Я уже молчу про такие варианты как зацикливание колбэков — первый сработал, второй сработал … десятый сработал и в результате его изменений повторно сработал первый колбэк. Или сработал сначала первый, потом третий, потом пятый, а за ним второй (нумерация здесь условна — просто для наглядности). Ни о какой контролируемой последовательности работы колбэков речи даже не идет. Каждый сам по себе.

История колбэков


У вас есть ордер. Его частично обработали, после чего вы выкатили очередное обновление приложения — логика поменялась — к вам приходит пользователь и задает все тот же вопрос — «почему здесь такое значение». Теперь вам нужно разбираться не только в текущем коде, но и смотреть, какая версия кода использовалась на момент предполагаемого срабатывания колбэка — предположений становится только больше.

Версии логики


Допустим вам нужно поменять логику колбэка. Например раньше при выполнении какого-то условия создавался один вид сервиса, теперь — другой. Но для уже обрабатывающихся ордеров (тех, что стартовали на старой логике и еще не до конца обработаны) — нужно сохранить старую логику.
Ваш колбэк разрастается — нужно заложить новую логику и сохранить старую, срабатывающую для ордеров, запущенных например до какого-то числа. У меня был случай когда бизнес-пользователи попытались до-процессить заказ, который «стоял» незавершенным 7 лет. Сколько логики и кода поменялось за это время — попробуйте только представить.

Колбэки ужасно мониторятся


Когда к вам придет бизнес-пользователь и задаст вопрос «а почему вот у этого сервиса вот здесь стоит такое-то значение» — вы сталкиваетесь с первой проблемой — вам нужно смотреть базу, логи и прочее.
Даже понять какой именно колбэк сработал и поменял интересующие вас данные может быть очень непростой задачей и на нее можно убить не один день. На самом деле сколько бы вы не копались в логах — в результате вы сможете только предположить что «сработал этот колбэк потому-то и потому-то». Даже если у вас есть логи, при разрастании системы вы все больше будете скатываться в предположения. Возможно несколько колбэков срабатывали и устанавливали одно и то же значение в эту модель. Тогда совсем плохо. Сложность анализа, воспроизведения и устранения проблемы растет в геометрической прогрессии.

Привязка к модели


Мой «любимый» грех колбэков — они жестко привязаны к модели. Сегодня у вас ордер TypeA, завтра его заменили ордером TypeB — и нужные колбэки не выстрелили — цепочка обработки ордера разломалась. Можно конечно ее починить — технически это ошибка кода, но на практике — лучше так строить свое приложение, чтоб эта ошибка в принципе не могла возникнуть.
Далее, положим у вас 2 состояния ордера — new и completed и связанные с ними колбэки. Вам нужно усложнить логику — добавить колбэков на время обработки логики. Например failed, processing — и повестиь на них еще несколько колбэков. Ордер может попасть в failed несколько раз за время своей обработки и может несколько раз перейти в processing — и каждый раз сработают колбэки.
Затем выясняется что логика после fail-а — отличается и появляется статус re-processing (напримре) и логики становится еще больше. Количество колбэков растет. При этом надо учесть что ордер оплачен или нет — и это еще несколько колбэков, привязанных к разным параметрам модели. Пока ордер не оплачен (paid — одно поле ордера, status — другое) сервисы запускать нельзя и т.д. — вы начинаете придумывать новые состояния ордера almost-done-not-yet-paid и т.п. — вы начинаете придумывать новые и новые состояния, чтоб к ним можно было приделать логику. Комбинаций состояний (оплачено / обработано, неоплачено / обработано и т.д.) становится больше и больше. Колбэки все сложнее.

Нет изолированности логики


Постараюсь пояснить — допустим у вас есть сервис мобильного интернета. Был первый заказ, по которому этот сервис был запущен, затем был второй заказ — изменение тарифного плана — сервис по сути тот же, но биллится по другому, имеют другую скорость интернета и т.д. На колбэках вы не можете изолировать логику, связанную с первым ордером, от логики, связанной со вторым ордером — у вас все привязано к модели сервиса как такового.
Более того — есть бухгалтерия — у которой бизнесс-логика связана с оплатой ордера, есть технический отдел, который должен обеспечить работу интернета — у него бизнесс-логика другая — и они на самом деле должны быть изолированы. На колбэках все завазяно на модель ордера — и разделить обработку бухгалтерии и технического отдела уже невозможно. Бухгалтерия может провести оплату сервиса несколькими платежами — несколько «процессов» —, а технический отдел за это же время может несколько раз прогнать тех-поддержку и какие-то сервисные операции для обеспечения одного и того же сервиса и вся эта логика не должна быть завязана на модели.

Вывод простой: логика приложения не должна быть привязана к модели.

Альтернатива


Первое что нужно понять — если у вас есть бизнесс-логика — значит привязывать ее нужно не к модели. Есть флоу обработки ордера — значит нужна сущность, к которой все это привязывается вместо модели. Не умничая назовем эту сущность «процессом».

Вся бизнесс логика привязана к процессу — как вариант — в виде операций. Т.е. технически вы должны иметь возможность открыть процессы, менявшие ордер — и посмотреть — когда они были созданы, как отработали, какие пользователи при этом участвовали, что менялось и что делалось, какие операции отработали (и почему) — т.е. у вас должны быть не просто домыслы —, а точная история хода процесса. Плюс — желательно бы иметь возможность смотреть по еще не завершенным процессам — какие еще операции могут быть выполнены в рамках процесса. Если что-то сломалось в процессе — нужно поправив данные / код — иметь возможность запустить процесс дальше (а не менять через консоль состояние какого-то объекта, чтоб сработали какие-то колбэки и процесс пошел дальше).

Далее — в вашем процессе участвует пользователь. Если приложение на колбэках — пользователь фактически выброшен из логики — зашел на какой-то ордер, поменял какие-то данные — колбэки стрельнули и ордер дальше обрабатывается. В случае с процессом вы можете ввести такое понятие как «пользовательская операция» — и говорить что какой-то процесс ждет тех или иных операций пользователя, что в ходе работы процесса были созданы и выполнены следующие пользовательские операции. В рамках «пользовательской» операции пользователь может поменять данные на нескольких моделях.

Далее нужна какая-то конфигурация процесса — возможность указать какие операции в нем содержатся, зависимости операций и т.д.

Процесс и операции должны иметь свой скоуп данных — какие-то промежуточные переменные процесса, которые позволяют не грузить модели данными, нужными только процессам. В результате модели проще и легче.

Пока на этом остановлюсь. Если тема интересная — то можно будет много чего еще написать. Кому интересно — смотрите gem rails_workflow и задавайте вопросы.

© Habrahabr.ru