Как стать автором
Обновить

Комментарии 24

Ну, возможно, потому, что finite state machine является частным случаем сетей Петри (по крайней мере если судить по енвики)

Не менее интересно делать системы, в которых путь workflow можно формировать динамически, например из веб-интерфейса. В этом случае кастомер сам конфигурирует состояния, возможные переходы и правила "допустимости" перехода из одного состояния в то или иное новое. Вот один из примеров моих систем: https://github.com/dobryakov/workflow-system


А вот и другая статья на тему, как можно отдать поток исполнения в руки кастомеру: https://www.dobryakov.com/blog/1838/

Данный компонент позволяет и динамически формировать и юзером, и админом, и в базе хранить и с внешних сервисов по API получать, и в env-переменных задавать, конфиг лишь способ удобного задания статических параметров. Впрочем это к экосистеме Симфони в целом применимо — конфиги лишь способ задания статических параметров и из любого поддерживаемого формата в итоге всё равно компилируются (именно компилируются, а не заполняются какие-то структуры данных) в php-код, который можно и ручками писать сразу и любую динамику в нём делать.

Давно посматриваю на компонент, но никак не могу встреть примеров реального использования в сложных процессах, не говоря о лучших практиках. Несколько вопросов на которые примеры плохо отвечают:


  • как лучше реализовать транзакции не просто состояние (place) процесса изменяющие, но и несущие полезную нагрузку, например отредактированную статью в транзакции "утверждение корректором"
  • как лучше реализовать воркфлоу, в которое вовлечено множество сущностей (возможно даже на старте флоу — неизвестное множество, которое к концу заполняется)
  • как лучше реализовать автоматическую транзакцию типа "публиковать", когда выполнены две типа "утверждена журналистом" и "утверждена корректором"

Такое ощущение, что его привязка к обычным сущностям — это лишь возможность для простейших случаев, которой лучше не пользоваться, а надо создавать отдельную сущность для хранения состояния именно процесса, которая уже как-то связана с состояниями участвующих в процессе сущностей. То есть на примере с пулл-реквестом нужно создавать отдельную сущность PullRequestWorkflow, которая с PullRequest связана один-к-одному, причём может даже односторонне, так что сам PullRequest о флоу и не знает. Но не понятно, изменение PullRequest должно быть реакцией на изменение (или попытку изменения) PullRequestWorkflow или наоборот.


Использовал кто на практике в сложных флоу?

Не уверен, что правильно понял все вопросы, но попробую предложить свои варианты решения.


как лучше реализовать транзакции не просто состояние (place) процесса изменяющие, но и несущие полезную нагрузку, например отредактированную статью в транзакции "утверждение корректором"

Использовать Command Bus. Создаётся 2 класса (команда и обработчик команды). Собственно команда и несёт в себе полезную нагрузку (пейлоад), это просто тупой класс с гетерами, а соответствующий обработчик производит манипуляции с этим пейлоадом в конце которой, происходит ->apply($article, 'next_state');
https://habrahabr.ru/post/280512


как лучше реализовать воркфлоу, в которое вовлечено множество сущностей (возможно даже на старте флоу — неизвестное множество, которое к концу заполняется)

К сожалению не понял, что имеется ввиду)


как лучше реализовать автоматическую транзакцию типа "публиковать", когда выполнены две типа "утверждена журналистом" и "утверждена корректором"

Евенты, команды, умная мидлваря для команд.


  1. Кидать евент try_to_publish в конце каждой комманды-транзакции (утверждение журналистом, утверждение корректором), в обработчике которого будет происходить проверка $workflow->can($article, 'publish'); и если тру, то кидаться команда PublishAtricle. Или продублировать проверку $workflow->can($article, 'publish'); в командах и если тру сразу кидать команду на паблишинг.


  2. Диспатчить команду PublishArticle, в конце каждой комманды-транзакции (утверждение журналистом, утверждение корректором), а проверку $workflow->can($object, 'state') вынести в миддлварю.


  3. Можно добавить в конфиг параметр auto: true/false для определённых переходов и написать умную мидлварю, которая будет проходиться по всем возможным переходам (те которые ->can($obj, 'state); //true) для сущности, и если переход помечен как auto: true, то диспатчить соответствующую команду.

Я лично за последний вариант)

Использовать Command Bus

Может вы про CQRS? Там не команды записываются а ивенты которые произошли в контексте агрегатов сущностей. Вот тогда да, тогда можно будет делать $article->apply($nextState);. А воспринимать команды как ивенты — будет работать опять же только на очень простых задачах. Да и будут проблемы потом с расширением логики.

Да, про CQRS, вернее про его часть.


К сожалению не понимаю что вы хотели сказать и что побудило)
Я не отождествлял команды с евентами, а писал про использование их в вместе в ответе на 3й вопрос. Да и то, только в 1м варианте, но в целом это никак не противоречит cqrs. К слову, cqrs очень хорошо сочетается с event-based programming.


Там не команды записываются а ивенты которые произошли в контексте агрегатов сущностей. Вот тогда да, тогда можно будет делать $article->apply($nextState);.

Можете расшифровать? Куда записываются, какие ивенты? Вы про event sourcing? Если да, то причём он тут?)


Происходит какая-то бизнес-логика и диспатчится комманда (со статьёй в кач-ве пейлоада) --> миддлваря проверяет может ли выполниться комманда $w->can($command->article, 'approved_by_spellchecker'); --> обработчик чё-то делает, а потом в конце $w->apply($command->article, 'approved_by_spellchecker');. Всё. Что тут не так?)

вернее про его часть.

Command Bus — не обязательная часть CQRS. Это скорее простой способ заставить разработчика делать CQS. Аля void GRASP контроллеры, которые декларируют флоу конкретного юзкейса. У Дядей Бобов например во всяких чистых архитектурах подобные штуки назывались Interactors. Можно их же воспринимать как сервисы уровня приложения.


К слову, cqrs очень хорошо сочетается с event-based programming.

ну потому что это оно и есть собственно. Если вы имеете в виду только разделение операций на "операции чтения" и "операции записи" то это CQS.


Куда записываются, какие ивенты? Вы про event sourcing? Если да, то причём он тут?)

Event sourcing это самый простой способ решения проблемы eventual consistency. Помимо него можно делать похожие но чуть другие штуки.


Записываются такие ивенты в какой-то сторадж. Обычно пишут в монгу или касандру но это не принципиально.


Всё. Что тут не так?)

  1. откуда статья возьмется в пэйлоаде? Кто и на каком уровне ее вытащит и добавит в пэйлоад? То есть мы должны достать сущность еще на уровень выше?
  2. Как обработчик команды сделает работу? Из вашего описания у нас есть некая эдакая процедура "approved_by_spellchecker" которая на вход получает данные (инстанс Article) и что-то с ней делает. То есть либо наш хэндлер команды просто сконвертит действие в сообщение самому Article либо для того что бы делать дела мы должны полностью сломать инкапсуляцию Article.

А ведь вопрос был в


как лучше реализовать автоматическую транзакцию типа "публиковать", когда выполнены две типа "утверждена журналистом" и "утверждена корректором"

если бы мы говорили в терминах CQRS то команды генерили бы нам на выходе сэт ивентов ApprovedByJournalist и ApprovedByProofreader. Еще одна штука бы подписывалась на эти события и при достижении этого стэйта либо инициировала бы следующий транзишен стэйта либо просто строила бы проекцию сущности и писала бы например в табличку опубликованных статей.

Блин, вы что-то всё усложнили сильно и зачем — непонятно)


Вопроса было 3, я дал 3 ответа, причём последний содержит 3 варианта. Возможно я плохо их отформатировал и вы подумали, что пронумерованный список это соответственные ответы на все 3 вопроса. Пронумерованный список относится только к последнему вопросу. И естественно, я не углублялся в детали и не расписывал весь флоу обработки запроса в вакууме с прикручиванием cqrs или cqs, ведь это не относится к вопросам.


откуда статья возьмется в пэйлоаде? Кто и на каком уровне ее вытащит и добавит в пэйлоад? То есть мы должны достать сущность еще на уровень выше?

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


Как обработчик команды сделает работу? Из вашего описания у нас есть некая эдакая процедура "approved_by_spellchecker" которая на вход получает данные (инстанс Article) и что-то с ней делает.

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


А ведь вопрос был в

Это был 3й вопрос. И для его решения я бы сделал post middleware, которая автоматически будет проверять сущности из пейлоада команд и диспатчить новые команды для авматических переходов. Или да, можно генерить сет евентов, о чём я говорил в 1м варианте ответа на 3й вопрос.

создастся какая-то команда, в которую положат статью

если у вас УЖЕ есть статья то вам не нужно городить команды. Мы УЖЕ потеряли весь профит от Command Bus и теперь нам намного проще будет вызывать методы сущности нежели городить еще один слой кода.


Опять, какая мне разница?)

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


А так мы просто размазали эту весьма тривиальную логику еще больше по системе. Что не есть гуд.

если у вас УЖЕ есть статья то вам не нужно городить команды. Мы УЖЕ потеряли весь профит от Command Bus и теперь нам намного проще будет вызывать методы сущности нежели городить еще один слой кода.

Почему? Где вы предлагаете дёргать методы сущности в соответствии с какой-то бизнес логикой?
В контролере? Контроллер раздуется, получится лапшекод и жесть, которую потом хз как переиспользовать. А команды можно кинуть будет и через джобы потом, если потребуется. И тестировать есть что. Да это я думаю вы и так понимаете.
В сервисах? Ну это кому как, на вкус как говорится) Ну кода ещё по-меньше будет.


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

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

В контролере? Контроллер раздуется, получится лапшекод и жесть, которую потом хз как переиспользовать.

но вы же уже достаете в контроллере репозиторий, достаете сущность по запросу и только потом формируете команду. Так что зоны ответственности уже размазаны.


Если что у меня обычно есть хэндлеры команд и команды, в них была бы только айдишка сущности а не вся сущность.


Если это запихать в сущность, то это получится не сущность, а сервис какой-то или вообще хз что.

доменные ивенты вам помогут. Не предлагаете же вы делать сущности тупыми property-bag-ами?


Сущность сама себя в базу записывать будет?)

репозитории, unit of work. Сущность ничего не знает о базе, она знает только о том что у нее есть стэйт и только ей дано знать как этот стэйт должен поменяться.

доменные ивенты вам помогут. Не предлагаете же вы делать сущности тупыми property-bag-ами?

Ну в моём представлении это проперти бэг + методы которые манипулируют этими проперти. Ну т.е. стейт и то как этот стейт менять, как и вы и упомянули в конце.


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

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


Сущность ничего не знает о базе, она знает только о том что у нее есть стэйт и только ей дано знать как этот стэйт должен поменяться.
Ваще с этим не спорю и не ставил под сомнения. К чему вы это всё пишете?)

Короче, какой-то холивар на пустом месте) VolCh спрашивал:


  1. Как организовать транзакции с пейлоадом? Я предложил команды (статья или айдишка статьи в пейлоаде — в данный момент не суть). И из всего что вы написали, я так и не понял, что здесь не так.


  2. Я не понял вопроса.


  3. Как сделать автонакатывание перехода? Я предложил евенты, миддлвари для команд.

Если хотите дальше продолжать, то давайте по пунктам и по делу, где и что не так. Или ещё лучше, предложите ваш вариант решения этих вопросов, что, думаю, будет всем интересно.

Ну т.е. стейт и то как этот стейт менять, как и вы и упомянули в конце.

я несколько другое упомянул. Я не про сеттеры, как возможность извне поменять стэйт объекта. Я про то что код использующий объект вообще ничего не должен знать о стэйте. Инкапсуляция и все такое.


Но как это всё относится к самым первоначальным вопросам?)

Моя претензия была именно к command bus так как проблему это не решает и из вашего описания создает новые.


Я предложил команды

это вопрос в контексте компонента workflow. Ваше предложение не отвечает на вопрос.


Я предложил евенты, миддлвари для команд.

ну или адаптеры и просто слать сообщения объектам. Речь опять же только про workflow компонент. потому ваше предложение юзать шину команд ни разу не отвечает на поставленные вопросы.

Я не про сеттеры

Я про сеттеры что ли? Я про нормальные доменные методы...


Моя претензия была именно к command bus так как проблему это не решает и из вашего описания создает новые.

А я пытаюсь всё это время узнать почему? и какие новые проблемы?


это вопрос в контексте компонента workflow. Ваше предложение не отвечает на вопрос.

На мой взгляд это вопрос в контексте приложения. Смысл рассматривать компонент в отрыве от приложения и того как он там используется?


Вопросы были:


  1. Как организовать транзакции в которые можно запихнуть какой-то пейлоад? Я так понял транзакции имелось ввиду не в терминах автора статьи… Что корректней называть переходом.


  2. Как сделать автопереход на следующий плейс, при достижении всех необходимых условий для перехода?

С command bus + workflow и транзакции с пейлоадом тебе, и автопереходы. И я понимаю, что это не единственный возможный вариант...

т.к. выглядит как побочный эффект.

так в этом же суть "команд". Они создают побочные эффекты. Любая операция связанная с взаимодействием с "внешним миром" по отношению к текущему процессу, например запись, и есть побочный эффект.

Тоже поглядываю на компонент, но не могу понять где его применять. У меня в голове несколько вопросов:


  1. Правильно ли я понимаю, что компонент workflow можно использовать для чего угодно, а не только для pull request? Просто везде на сайтах именно этот пример показывают.
  2. Не проще ли иметь просто поле с состоянием, чтобы его переключать? В чем удобство?
  1. Да, ещё примеры на статьях бывают :)
    2.1. Ограничивается граф возможных направлений перехода из одного состояния в другое, чего "тупой" setStatus сделать не может.
    2.2. Граф можно задавать в конфиге, а не в коде
    2.3. Можно иметь одновременно несколько состояний для параллельных подпроцессов
    2.4. "Бесплатная" интеграция через события с остальным приложением.

как-то так

+ Визуализация
+ Guads (3.3)

2.2. Граф можно задавать в конфиге, а не в коде


Вот чего я не могу понять — а можно ли хранить граф НЕ в коде. Если можно, то тогда можно будет делать крутые штуки для бизнес-процессов, а-ля workflows в JIRA

Можно хранить всё как хочешь и где хочешь, и формировать динамически.

Нашел, действительно, спасибо
Это не просто переключалка. Это гарантия того, что объект всегда будет валиден с точки зрения бизнес логики приложения.
Возможно ли реализовать reject_by_journalist и reject_by_spellchecker в примере Article?
Например в Article уже установлен
["wait_for_journalist", "approved_by_spellchecker"]
журналист зареджектил. Статья отправилась в черновик.
Но 'currentPlaces' имеет значение
["approved_by_spellchecker", "draft"]

Ладно. Пускай.
Позже статья снова отправляется на ревю. 'currentPlaces' имеет значение
["wait_for_journalist", "wait_for_spellchecker", "approved_by_spellchecker"]
Парадокс однако. Ожидается апрув от Spellchecker, который уже и заапрувил.
В этот раз журналист заапрувил, а Spellchecker зареджектил. Состояние currentPlaces:
["approved_by_spellchecker", "approved_by_journalist", "draft"]

Статья обозначена как черновик при двух апрувах.
Возможно как-то из-коробки «подчищать» подобное состояние, или это нужно реализовывать самому?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории