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

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

Абсолютное большинство программистов начнет делать такое приложение на колбэках или тригерах. Создан новый ордер — ставим ему состояние 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 и задавайте вопросы.

Комментарии (17)


  1. maovrn
    30.06.2015 14:16

    Спасибо, хорошая тема поднята. Я только не уловил, как запускаются процессы в стандартной ситуации (помимо исключительного случая запуска руками). Опять же колбэком?


    1. madzhuga Автор
      30.06.2015 14:28

      Не обязательно. Процессы можно запускать откуда угодно. Можно например из контроллера — создали ресурс (ордер, например) — и для него запустили процесс.

      RailsWorkflow::ProcessManager.start_process(
        process_template_id , { resource: resource }
      )
      


  1. fzn7
    30.06.2015 15:29
    +1

    Паттерну «Команда» (http://c2.com/cgi/wiki?CommandPattern ) уже 20 с гаком лет. Организуйте команды в очереди и ваши проблемы будут решены


    1. madzhuga Автор
      30.06.2015 15:45
      +3

      Речь не об отдельном паттерне. Организуйте команды в очереди, возможность гнать их синхронно-асинхронно, пользовательские команды (пользуясь вашей терминологией), добавьте возможность конфигурировать «очереди», отслеживать «свалившиеся» очереди, экспорт-импорт очередей, поддержку версий конфигурации с учетом еще не завершенных процессов на старой версии конфигурации и т.д. В общем я не о реализации отдельно взятого паттерна а о движке на основе этой идеи.
      Паттерну 20 лет — а попробуйте найти нормальный движок на рельсах — и упретесь в то что его почему-то нет. Гемов для callback-ов — всяких state machine и прочих модификациях — полно. А чего-то посложнее — почему-то нет.


      1. fzn7
        30.06.2015 16:15

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


        1. madzhuga Автор
          30.06.2015 16:39

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


          1. fzn7
            30.06.2015 19:04

            Я согласен. Если кому-то пригодится то супер


        1. j_wayne
          30.06.2015 17:37
          +2

          >> там учат работать на коллбэках
          А там не учат, как потом поддерживать такое приложение?


          1. fzn7
            30.06.2015 19:01

            А что за выпад в мою сторону такой? Может они специально на коллбэках пишут что-бы потом $ рубить на поддержке. Я в тему глубоко не копал, посчитал необходимым обозначить уже готовое решение под озвученную тему, в чем проблема?


            1. j_wayne
              30.06.2015 20:03

              Не хотел вас задеть. В коллбеках проблема, наплакались кровавыми слезами в свое время.


            1. j_wayne
              30.06.2015 20:16
              +2

              Рекомендую интересующимся данной темой посмотреть Architecture the Lost Years by Robert Martin
              Не совсем это про коллбеки… Но в рельсах очень много таких моментов.

              А также рекомендую ознакомиться с блогами Роберта Мартина:
              blog.cleancoder.com
              blog.8thlight.com/uncle-bob/archive.html

              С DHH (автором рельсов) у них занятный спор вышел


  1. AnnieOmsk
    01.07.2015 16:51
    -2

    Вы сейчас открыли для себя новый чудный мир акторов, паттернов потоковой обработки и так далее :-) Сразу скажу, что эта история полностью альтернативна Rails, и вам будет очень сложно увязать одно с другим.

    Это объясняет миграцию разработчиков с Rails на Erlang, Clojure, Scala и так далее. Однако сами по себе языки не панацея, интересна сама модель акторов, на которой построен принцип работы Erlang-приложений и фреймворк Akka.

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


    1. madzhuga Автор
      02.07.2015 14:02
      +1

      Пока что не вижу никакой сложности. Все очень спокойно ложится на рельсы. Они в общем-то никоим образом не мешают использовать паттерны. Проблема в том что большинство берет туториалы по рельсам — и как в них описано — так и пишут (пример — те же callback-и).


      1. AnnieOmsk
        02.07.2015 16:34

        Попробуйте написать код, который полностью удовлетворяет SOLID на рельсах :-)

        Ну и модель акторов вы туда никак не прикрутите при всем желании.


        1. madzhuga Автор
          02.07.2015 16:44

          Не совсем понятна постановка вопроса :) «Попробуйте прикрутить». Вопрос — зачем? Я исхожу из задачи — отделить бизнес логику от моделей / контроллеров, сформировать из отдельных операций какой-то ход процесса и дать возможность с процессом работать (мониторить, саппортить и т.д.).
          В программировании очень много принципов, подходов и т.д. — что ж их все пробовать прикрутить? У меня есть задача, есть вариант решения. О нем и речь.


          1. AnnieOmsk
            02.07.2015 19:35
            -1

            Никто не говорит, что надо прикручивать все подряд. Но если начать как следует копать, то выяснится, что единственный математически обоснованный способ писать расширяемый код на современных вычислительных системах — это модель акторов. И это, конечно, не описывается словом «прикрутить» в обычном понимании этого слова. Оно возникло только из сочетания с рельсами. Потому что там либо выкинуть все рельсы, либо «прикрутить».

            У вас просто есть два пути. Первый — пробовать все самому, решая все усложняющиеся задачи и придя в итоге к той же модели, но через год или больше. Второй — попробовать узнать побольше, разобраться в теории и существенно сократить этот путь, повысив качество решений, которые вы сможете создавать. Выбор, безусловно, за вами :-)


        1. madzhuga Автор
          02.07.2015 16:49

          По поводу модели акторов — вроде как народ изголяется — https://github.com/celluloid/celluloid. Мне сложно судить насколько это удачное / неудачное решение но тем не менее оно есть.