Это первая статья из серии BPMN: Beyond the Basics – о скрытых нюансах и подводных камнях BPMN для разработчиков. В отличие от аналитиков, разработчикам надо не просто знать нотацию, но понимать, как реализован тот или иной ее элемент. А тут, как говорится, не все так однозначно.
Для начала возьмем самый простой – шлюз ИЛИ (Exclusive Gateway). На первый взгляд, всё очевидно: ставишь ромбик, рисуешь стрелочки – и вуаля! Но что происходит внутри движка? Как он выбирает путь выполнения? Что делать, если несколько условий срабатывают одновременно? А если ни одно не выполняется? В этой статье мы разберем эти вопросы и рассмотрим особенности реализации и использования этого элемента.
Разбираться будем на примерах в Jmix BPM с движком Flowable, но принципы универсальны – нотация BPMN 2.0 едина, и основные механизмы работы элементов схожи во всех движках, частности в Camunda 7. Об отличиях, если они встретятся, будем говорить особо.
Мы все учились понемногу…
Все когда-то проходили блок-схемы алгоритмов. Как вы помните, ромбик на блок-схеме обозначает условный оператор IF, из которого может быть только два выхода: ИСТИНА и ЛОЖЬ. Как мне кажется, именно по этой причине аналитики и разработчики, когда ставят exclusive gateway на диаграммах, на автомате так формулируют вопросы, чтобы ответы были «да» или «нет».
И очень зря так делают! Из-за этой школьной привычки BPMN-модели часто получаются избыточно громоздкими, потому что их авторы пытаются смоделировать принятие решений исключительно в логике «да-нет». На самом деле, exclusive gateway скорее похож на оператор SWITCH, чем на IF, у него может быть несколько выходов:

Вот как выглядел бы exclusive gateway в коде:
switch (forecast) {
case "rain" -> advise = "Take an umbrella";
case "snow" -> advise = "Wear warm clothes";
case "sunny" -> advise = "Wear sunglasses and sunscreen";
case "windy" -> advise = "Hold onto your hat";
case "storm" -> advise = "Stay indoors if possible";
default -> advise = "Check the forecast for better advice";
}
Исходящих потоков может быть сколько угодно, хоть 100500. Конечно, такую схему едва ли легко будет читать, но движку-то все равно! Процесс может вовсе не иметь графического представления и тем не менее он будет работать. (Да, такое возможно. Просто удаляете из XML-файла процесса раздел описания диаграммы, который начинается с тега <bpmndi>
и все.)
Продолжим наш предыдущий пример. Если следовать шаблону IF-ELSE, то вопрос о дожде с тремя вариантами ответа превратится в такую конструкцию:

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

Разводящие разводят, сводящие сводят
Задача любого шлюза не задавать вопросы, а управлять разветвлением процесса. То есть, позволять процессу разойтись на несколько путей, которые потом могут сойтись обратно. Или не сойтись, так тоже бывает.
По своей роли в процессе шлюзы ИЛИ бывают разводящие (fork) и сводящие (join). Хорошей практикой считается эти роли не смешивать, пусть каждый шлюз занимается чем-то одним. Но, увы, довольно часто встречается, когда один шлюз выполняет обе роли:

Никогда так не делайте! Шлюз должен иметь одну роль. Не жадничайте, ставьте больше шлюзов, чтобы было четко понятно, за что каждый отвечает.

К сожалению, нотация не запрещает шлюзу иметь много входов и выходов. Поэтому даже такая страшная конструкция, как на рисунке ниже, будет работать:

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

Что у него внутри?
Внутри у Exclusive Gateway ничего нет. То есть нет никаких свойств или параметров, которые можно было бы установить или настроить. Ничегошеньки, только ID и name. Аналитики, когда пишут свои вопросы над ромбиками шлюзов на самом деле всего лишь дают этому элементу текстовую метку, которая на ход процесса никак влиять не может.
Например, вот:

А внутри XML-файла BPMN наш шлюз будет выглядеть так:
<exclusiveGateway id="Gateway_02z3qk1" name="Будет ли сегодня дождь?"/>
Об условиях
Условие определяется в свойствах стрелки, то есть, объекта sequence flow, на диаграмме его не видно:

Результатом вычисления условия должно быть логическое значение true или false, в противном случае вылетит ошибка.
Вот несколько примеров:
${orderAmount > 1000}
${price > 100 && price <= 500}
${accountant.username == "jane"}
— Насколько сложным может быть условие на шлюзе?
Вообще-то, чем проще, тем лучше. Однако, если логика действительно сложная, то лучше упаковать ее в код, а в условии вызвать соответствующий метод, например вот так:
${weatherService.getForecastCondition()}
Позвольте, как же он работает?
Сам шлюз внутренней логики не имеет. Все логика – на исходящих из него стрелочках, или, говоря формально, потоках управления (sequence flow). Но и здесь не надо путать текстовую метку, которая видна на схеме и условие, которое проверяется движком.

Когда процесс доходит до нашего шлюза, он смотрит на исходящие потоки и вычисляет логические выражения, установленные на них. Во всех учебниках написано, что первое условие, которое окажется истинным и определяет, по какому пути процесс двинется дальше.
— Да вообще тривиально! О чем тут говорить?
— Хм. А что значит «первое»? У них же нет номеров.
— И как же узнать?
—А давайте посмотрим в XML-файле:
<exclusiveGateway id="Gateway_18tv2av" name="Будет ли сегодня дождь?">
<incoming>Flow_0hpfy8t</incoming>
<outgoing>Flow_1dxinb9</outgoing> (1)
<outgoing>Flow_1k36sg0</outgoing> (2)
</exclusiveGateway>
— Ага, все ясно! Первый исходящий будет Flow_1dxinb9
, а второй Flow_1k36sg0
.
— А вот и нет! Это работает по-другому.
Когда процесс доходит до шлюза, движок читает список ID исходящих потоков и идет искать их определения. Тот поток, который встретится раньше и будет первым.
Вообще говоря, определения потоков могут находиться в XML-файле где угодно, вовсе необязательно сразу после шлюза. Элементы в модели лежат в том порядке, в котором их создавали. Когда вы интенсивно редактируете процесс, добавляя и удаляя кубики и стрелочки, в XML-файле все будет изрядно перемешано. Впрочем, движку это безразлично.
Окей, вернемся к нашему exclusive gateway и найдем определения его потоков. Кстати, держите лайфхак: если давать BPMN-элементам осмысленные айдишники, то модель будет более читабельной и с логами тоже будет проще работать.
Как, например, здесь:
<exclusiveGateway id="Gateway_is_rain" name="Будет ли сегодня дождь?">
<incoming>Flow_incoming</incoming>
<outgoing>Flow_rain</outgoing>
<outgoing>Flow_clear</outgoing>
</exclusiveGateway>
<sequenceFlow id="Flow_incoming" sourceRef="startEvent1" targetRef="Gateway_is_rain" />
<sequenceFlow id="Flow_rain" name="Да" sourceRef="Gateway_is_rain" targetRef="Activity_script_1">
<extensionElements>
<jmix:conditionDetails conditionSource="expression" />
</extensionElements>
<conditionExpression xsi:type="tFormalExpression">${forecast == "rain"}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="Flow_clear" name="Нет" sourceRef="Gateway_is_rain" targetRef="Activity_script_2">
<extensionElements>
<jmix:conditionDetails conditionSource="expression" />
</extensionElements>
</sequenceFlow>
Теперь четко видно, кто первый, а кто второй. И если элементы <sequenceFlow>
поменять местами, то изменится и порядок обработки – второй станет первым, а первый вторым:
<exclusiveGateway id="Gateway_is_rain" name="Будет ли сегодня дождь?">
<incoming>Flow_incoming</incoming>
<outgoing>Flow_clear</outgoing>
<outgoing>Flow_rain</outgoing>
</exclusiveGateway>
<sequenceFlow id="Flow_incoming" sourceRef="startEvent1" targetRef="Gateway_is_rain" />
<sequenceFlow id="Flow_clear" name="Нет" sourceRef="Gateway_is_rain" targetRef="Activity_1odfau0">
<extensionElements>
<jmix:conditionDetails conditionSource="expression" />
</extensionElements>
</sequenceFlow>
<sequenceFlow id="Flow_rain" name="Да" sourceRef="Gateway_is_rain" targetRef="Activity_0agmpfb">
<extensionElements>
<jmix:conditionDetails conditionSource="expression" />
</extensionElements>
<conditionExpression xsi:type="tFormalExpression">${forecast == "rain"}</conditionExpression>
</sequenceFlow>
Как видно из этого примера, BPMN хоть и графическая нотация, но в некоторых ситуациях надо смотреть в код, картинки не всегда могут быть интерпретированы однозначно.
Пусто – значит «Да»
Наверное, вы заметили, что для второго исходящего потока у нас никакое условие не задано. И как думаете, что в таком случае произойдет?
Если условие не задано, оно считается истинным. То есть, когда мы поменяем определения потоков местами, движок даже не дойдет до вычисления выражения ${forecast == "rain"}
, он просто пойдет по пути с меткой Flow_clear
.
Довольно часто случается, что разработчик забыл поставить условия на выходах из шлюза ИЛИ и процесс начинает вести себя странно – все время сворачивает не в ту сторону, хотя значения переменных правильные и по логике должно быть иначе.
К сожалению, BPM-движки Camunda и Flowable при деплое процесса не проверяют, чтобы условия на исходящих потоках были заданы, поэтому эту ошибку бывает трудно поймать.
Истин может быть много
Может ли одновременно несколько условий быть истинными? — Очевидно, что да. Это прямо следует из того, что мы говорили выше про порядок обработки. Движок вычисляет их по очереди, условия между собой никак не связаны. Но сработает только одно. Однако, поскольку в реальности трудно контролировать этот порядок – это ж надо каждый раз смотреть в XML, то лучше все-таки чтобы условия были взаимоисключающими.
Если вам нужно реализовать логику, когда действительно может выстрелить несколько вариантов, то можно вспомнить про Inclusive Gateway, который это умеет:

А еще стоит подумать про таблицы решений DMN, на них можно реализовать практически любую логику выбора нужного варианта, причем даже без написания кода.
Вообще говоря, DMN — это мощный и сильно недооцененный инструмент, о нем еще поговорим отдельно.
Поток по умолчанию
Разумеется, вы знаете, что рекомендуется один из исходящих потоков назначать потоком по умолчанию. Потому что если ни одно из условий не окажется истинным, то движок выбросит исключение и процесс остановится. Поэтому лучше направить процесс хоть куда-то, чем вылетать по ошибке.
Кстати, а что будет если на потоке по умолчанию задано какое-то условие? — Ровным счетом ничего, движок его проигнорирует.
На диаграммах поток по умолчанию обозначается косой черточкой:

В общем, все точно так же, как в операторе SWITCH.
Избегайте каскадов
Бывает, что вопросов как бы несколько. И тогда рука так и тянется поставить несколько шлюзов. Но не спешите! Посмотрите внимательно – если на этом пути реальных развилок нет, а просто идет уточнение ответа с отсечкой негодных вариантов, то можно не городить каскад из шлюзов ИЛИ, а вместо этого написать более сложное условие.

Как, например, здесь:

Условие получается сложнее, за то диаграмма проще. И мы же помним. что сложное условие можно реализовать в виде метода и тогда выражение будет простым:
${isSpringComing()}
Давайте подытожим:
Внутри себя Exclusive Gateway не содержит никакой логики.
Его название служит только для лучшего понимания модели человеком, для движка оно не имеет значения.
Exclusive Gateway может иметь сколько угодно выходов, это аналог оператора SWITCH, а не IF.
Поэтому не надо строить каскады из шлюзов ИЛИ, формулируйте исчерпывающие условия, чтобы не задавать много вопросов.
Назначайте один из исходящих потоков потоком по умолчанию.
Условия содержатся в свойствах исходящих потоков, между собой они формально никак не связаны.
Движок не проверяет, чтобы эти условия были взаимоисключающими, они могут быть хоть одинаковыми, это на ответственности разработчика.
Движок проверяет условия в том порядке, в котором описания исходящих потоков хранятся в XML-файле модели.
Как только одно из условий будет истинно, процесс двинется дальше по этой ветке, остальные условия проверяться не будут.
Если никакое из условий не оказалось истинно, будет выбран поток по умолчанию.
Вот, пожалуй, и все! Теперь вы точно познакомились со шлюзом ИЛИ. Другие элементы BPMN тоже имеют свои сюрпризы, и мы продолжим о них говорить в следующих статьях.
Jmix.ru — платформа быстрой разработки B2B и B2G веб-приложений на Java.
BPM Developers — про бизнес-процессы: новости, гайды, полезная информация и юмор.