Как рисовать Sequence без боли и страданий в PlantUML

Привет! Меня зовут Настя, я старший системный аналитик в X5 Tech. Я рисую sequence-диаграммы каждый день на протяжении четырёх лет. За это время я прошла все круги ада по Данте, то есть попробовала разные инструменты для рисования этих самых диаграмм. Пока не встретила его — PlantUML. 

Что удивительно, инструмент довольно не новый, но тем не менее лучше него я пока не встречала. А ещё удивительно, что он не особо популярный. Когда мы запустили в управлении системного анализа первый воркшоп по PlantUML, за 3 минуты после анонса пришли 12 заявок от аналитиков разных грейдов — от Junior до Lead. 

В процессе подготовки материалов к воркшопу мы искали статьи и литературу, которые помогли бы дополнительно изучить sequence-диаграммы в PlantUML. Ничего интересного мы не нашли. 

На самих воркшопах участники часто говорили о том, что они пытались самостоятельно изучить PlantUML, но их пугало то, что нужно писать какой-то код и учить какой-то синтаксис. Документация достаточно обширная, но информации о том, как последовательно строить sequence почти нет. 

Поэтому и появилась эта статья. 

Почему PlantUML?

Первый раз я увидела PlantUML, когда работала на аутстаффе. Я была на проекте в банке, где занималась проектированием взаимодействия бэкенда и смежных систем. 

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

Я пошла к ПМу, чтобы узнать, как построено взаимодействие между компонентами системы. Предвкушая ваши вопросы, сразу обозначу, что ПМ на том проекте был сильно подкован технически и лучше всех в команде понимал, как работает система. 

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

Почему я так быстро переключилась на PlantUML?  

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

Как-то я даже рисовала sequence в Miro. Там мне понравилось больше всего из всех подобных платформ, потому что в Miro всё красивенько получается. 

Потом я узнала, что PlantUML — не единственный язык, который позволяет рисовать Sequence. Например, есть ещё D2 или MermaidJS. Есть ещё штука https://sequencediagram.org/, но, как по мне, она совсем на любителя. 

Я использую PlantUML по следующим причинам:  

  1. Не надо рисовать стрелочки.

  2. Мы ведём документацию в Confluence, и там есть плагин, в котором я всё и пишу. Это удобно. 

  3. Ничего лишнего. В PlantUML можно сделать диаграмму, которая будет соответствовать стандартам sequence и при этом не будет ничем перегружена. 

  4. Неплохая документация. В ней описано всё, что можно сделать, да ещё и с примерами.

Мой алгоритм построения диаграмм

Дополнительно всё можно найти в этих ваших Интернетах. 

Для примера возьму заказ кофе в кофейне. Это будет простая диаграмма, которые обычно мы не рисуем. В Sequence важно показать взаимодействие между компонентами системы или между несколькими системами. У меня задача другая — я не хочу писать, как правильно рисовать диаграммы. Я хочу показать, что PlantUML классный инструмент. 

Первый шаг — плагин в Confluence. Ищете PlantUML, выбираете, добавляете заголовок и у вас на странице появляется полотно для творчества. 

Второй шаг — написать @startuml и @enduml. Я не нашла сокровенной тайны, почему так надо делать (если знаете, напишите в комментариях). Я их всегда пишу в знак уважения к синтаксису, но без них диаграмма прекрасно будет отображаться.

@startuml 

@enduml

27ab1b0182099b68997f81e21f1538e9.png

Третий шаг — определение участников. Обычно конструкция определения участника выглядит так:  

<тип участника> <название> <опции>

Чаще всего я использую следующие типы участников:  

  1. actor — рисуется в виде человечка, обозначает действующее лицо.

  2. participant — рисуется в виде прямоугольника, обозначает какой-то компонент системы.

  3. database — рисуется в виде значка базы данных и обозначает эту самую базу данных.

  4. queue — рисуется в виде значка очереди и обозначает эту самую очередь.

Название можно писать на любом языке, но есть нюанс — если в вашем названии есть пробелы, то тогда всю конструкцию надо заключить в кавычки. Или между словами добавить нижнее подчёркивание, но этот вариант мне не очень нравится. 

Опции, которые я использую:  

  1. псевдонимы as;

  2. сортировка order.

Псевдонимы — очень удобная штука. Если у вас есть длинные названия или названия на русском языке, то вы можете их использовать. Выглядит так:  

<тип участника> <название> as <сокращенное_название>

Это сокращённое название нужно потом использовать в коде. Это удобно по двум причинам:  

  1. если название на русском, то не придётся постоянно переключать раскладку;

  2. если название длинное, его можно заменить одной или несколькими буквами и сэкономить кучу времени.

Есть важная деталь — в каком порядке вы напишете участников, в том порядке они и будут отрисованы. Если вам это неудобно, то можете использовать ключевое слово order и порядок участника. Опция order идёт после опции as.  

@startuml

actor Покупатель as cus order 1
actor Бариста as bar order 2
participant "Кассовый аппарат" as cash order 3
database Холодильник as ref order 5
participant "Рабочее место" as work order 4

@enduml

dd70d77d521271ef8d05bf239df41074.png

Четвёртый шаг — добавляем в диаграмму стрелки сообщений. Тут всё просто — берём участников, добавляем между ними стрелки и называем сообщение, которым они обмениваются. Конструкция такая:  

<участник><стрелка><участник>: <сообщение>

Стрелки тоже бывают разных видов, все они используются в диаграммах:  

  1. синхронное сообщение обозначается →

  2. асинхронное сообщение обозначается →>

  3. ответное (или возвращаемое) сообщение обозначается -->

  4. собственное сообщение обозначается комбинацией участник → участник

Ответное (или возвращаемое) сообщение можно оформить двумя способами. 

Первый способ:  

участник1 → участник2

участник1 <-- участник2

Второй способ:  

участник1 → участник2

участник2 --> участник1

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

@startuml

actor Покупатель as cus order 1
actor Бариста as bar order 2
participant "Кассовый аппарат" as cash order 3
database Холодильник as ref order 5
participant "Рабочее место" as work order 4

cus -> bar: сделать заказ
bar -> ref: проверить наличие продуктов
bar <-- ref: продукты в наличии
bar -> cash: выставить оплату
cash -> cash: сформировать оплату
bar <-- cash: оплата готова
cus <-- bar: просьба оплатить заказ
cus -> bar: оплата
bar -> cash: оплата
bar <-- cash: принято
bar -> ref: нужно молоко
bar <- ref: вот молоко
bar -> work: приготовление кофе
bar <-- work: кофе готов
cus <-- bar: вот ваш кофе

@enduml

c33789b60eb9a2b9ed413ed73b44e48b.png

Пятый шаг — добавляем альтернативные и опциональные сценарии. Можно ещё добавлять циклы и блоки, которые выполняются параллельно. 

Все такие блоки начинаются с ключевого слова, обозначающего вид блока, и заканчиваются ключевым словом end. Конструкция такая:  

<вид_блока><название_блока>

<что-то происходит>

end <вид_блока>

Первый вид блока — alt. Это альтернативный сценарий, обязательно с одной веткой else. Будет выглядеть примерно так:  

alt альтернативный сценарий

участник1 → участник2: сообщение

else другая ветка

участник1 → участник2: сообщение

end alt

Если вам нужен просто alt без else, то можете использовать второй вид блока — opt. Это опциональный сценарий, который может выполниться при определённом условии. Выглядит так:  

opt какое-то действие

участник1 → участник2: сообщение

end opt

Третий вид блока — loop. Действия в нём повторяются несколько раз или какой-то период времени. Выглядит аналогично opt:  

loop 3 раза

участник1 → участник2: сообщение

end loop

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

group какая-то группа

участник1 → участник2: сообщение

end group

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

@startuml

actor Покупатель as cus order 1
actor Бариста as bar order 2
participant "Кассовый аппарат" as cash order 3
database Холодильник as ref order 5
participant "Рабочее место" as work order 4

cus -> bar: сделать заказ
bar -> ref: проверить наличие продуктов
bar <-- ref: продукты в наличии
bar -> cash: выставить оплату
cash -> cash: сформировать оплату
bar <-- cash: оплата готова
cus <-- bar: просьба оплатить заказ
loop пока не будет оплачен заказ
cus -> bar: оплата
bar -> cash: оплата
bar <-- cash: принято
end loop
group приготовление кофе
alt заказали кофе на альтернативном молоке
bar -> ref: нужно альтернативное молоко
bar <- ref: вот молоко
else заказали кофе на обычном молоке
bar -> ref: нужно обычное молоко
bar <- ref: вот молоко
end alt
bar -> work: приготовление кофе
bar <-- work: кофе готов
end group
cus <-- bar: вот ваш кофе
opt покупатель оставил чаевые
cus -> bar: вот чаевые
cus <-- bar: большое спасибо
end opt

@enduml

95248ec06299f893b6225cb5b2dc05d0.png

Шестой шаг — активации. Это те самые плашечки, которые обозначают экземпляры выполнения операции. Их можно обозначать двумя способами:  

  1. ключевыми словами activate <участник> и deactivate <участник> — там, где вы их указываете, начинается и заканчивается активация.

  2. конструкциями ++ и --

Ключевые слова activate и deactivate указываются после строки с сообщением, которую вы хотите активировать или деактивировать. 

Конструкции ++ и — выглядят так:  

участник1 → участник2++: сообщение

участник2 → участник 3--: сообщение

Как вы заметили, плюсы и минусы указываются только на втором участнике. Это важный нюанс. Нельзя указывать так:  

участник1++ → участник2++: сообщение

Для такой конструкции будет ошибка. Если вам нужно активировать участник1, то после сообщения нужно указать activate участник1 или участник1++. 

Я в своих диаграммах использую только плюсы и минусы. Ключевые слова занимают много места. 

@startuml

actor Покупатель as cus order 1
actor Бариста as bar order 2
participant "Кассовый аппарат" as cash order 3
database Холодильник as ref order 5
participant "Рабочее место" as work order 4

cus -> bar++: сделать заказ
cus++
bar -> ref++: проверить наличие продуктов
bar <-- ref--: продукты в наличии
bar -> cash++: выставить оплату
cash -> cash: сформировать оплату
bar <-- cash--: оплата готова
cus <-- bar: просьба оплатить заказ
loop пока не будет оплачен заказ
cus -> bar: оплата
bar -> cash++: оплата
bar <-- cash--: принято
end loop
group приготовление кофе
alt заказали кофе на альтернативном молоке
bar -> ref++: нужно альтернативное молоко
bar <- ref: вот молоко
else заказали кофе на обычном молоке
bar -> ref: нужно обычное молоко
bar <- ref--: вот молоко
end alt
bar -> work++: приготовление кофе
bar <-- work--: кофе готов
end group
cus <-- bar: вот ваш кофе
opt покупатель оставил чаевые
cus -> bar: вот чаевые
cus <-- bar--: большое спасибо
cus--
end opt

e0100337367bdfcff1769d5d8f5659ba.png

Седьмой шаг — украшения. Их очень много в PlantUML, я расскажу только про те, которыми пользуюсь я. 

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

autonumber <число_с_которого_начнется_нумерация><шаг>

Если вы не указываете два параметра, то автонумерация по умолчанию начинается с цифры 1 с шагом 1. 

Второе — разделители. Очень удобно, когда большая диаграмма, и нужно поделить её на части. Конструкция такая:  

==<заголовок>==

Третье — задержка. Нужна, чтобы обозначить, например, что сообщение нужно отправить на следующий день. Конструкция такая:  

…<заголовок>…

Четвёртое — разделение сообщений. Если у вас есть большие сообщения, которые  удлиняют вашу диаграмму, то их можно разделить. Тоже есть два способа:  

  1. автоматический — с помощью команды skinparam maxMessageSize <число>, указывается в начале диаграммы и действует на все сообщения;

  2. ручной — с помощью команды \n, пишите в то место, где нужно поделить сообщение.

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

@startuml

actor Покупатель as cus order 1
actor Бариста as bar order 2
participant "Кассовый аппарат" as cash order 3
database Холодильник as ref order 5
participant "Рабочее место" as work order 4

autonumber

cus -> bar++: сделать заказ
cus++
bar -> ref++: проверить наличие продуктов
bar <-- ref--: продукты в наличии
==оплата==
bar -> cash++: выставить оплату
cash -> cash: сформировать оплату
bar <-- cash--: оплата готова
cus <-- bar: просьба\nоплатить заказ
|||
loop пока не будет оплачен заказ
cus -> bar: оплата
bar -> cash++: оплата
bar <-- cash--: принято
end loop
==готовка==
group приготовление кофе
|||
alt заказали кофе на альтернативном молоке
bar -> ref++: нужно альтернативное молоко
bar <- ref: вот молоко
else заказали кофе на обычном молоке
bar -> ref: нужно обычное молоко
bar <- ref--: вот молоко
end alt
bar -> work++: приготовление кофе
bar <-- work--: кофе готов
end group
...через 3 минуты...
cus <-- bar: вот ваш кофе
opt покупатель оставил чаевые
cus -> bar: вот чаевые
cus <-- bar--: большое спасибо
cus--
end opt

@enduml

c7b97b763c3a63ec4c07872230ae2776.png

PlantUML и ChatGPT

ChatGPT прекрасно может работать с PlantUML. Тут работает классическое правило — хотите хороший результат, напишите детальный запрос. Для примера можем попросить просто написать код для PlantUML с sequence-диаграммой работы бариста.

Запрос: Напиши код для plantuml с sequence-диаграммой работы бариста.

@startuml
actor Customer
actor Barista
participant Queue
participant Database

Customer -> Queue: Заказать кофе
Queue -> Barista: Передать заказ
Barista -> Database: Записать заказ
Database --> Barista: Подтверждение записи
Barista -> Barista: Проверить наличие ингредиентов
Barista -> Database: Обновить количество ингредиентов
Database --> Barista: Подтверждение обновления
Barista -> Barista: Приготовить кофе
Barista -> Queue: Сообщить о готовности заказа
Queue --> Customer: Оповестить о готовности

Customer -> Barista: Забрать заказ
Barista -> Customer: Передать кофе
Customer -> Barista: Оставить чаевые
@enduml

3b97d556510d63831d86afe0d263cd0f.png

Результат плохой: непонятно, что это за участники, не хватает активаций. Расположение участников тоже странное, если очередь — это физическая очередь, то зачем она вообще здесь. 

Но можно попробовать сформировать более конкретно. 

Запрос: Мне нужен код sequence-диаграммы для plantuml. В диаграмме должно быть 5 участников: покупатель, бариста, касса, рабочее место и холодильник. Обязательно должны быть активации и деактивации, один альтернативный сценарий с выбором молока (альтернативное или обычное) и опциональный блок для оставления чаевых. Хорошо бы добавить нумерацию сообщений. 

@startuml
autonumber
actor Customer
participant Barista
participant CashRegister as "Cash Register"
participant WorkStation as "Work Station"
participant Fridge as "Fridge"

Customer -> Cash Register: 1. Сделать заказ
activate Cash Register
Cash Register -> Barista: 2. Передать заказ
deactivate Cash Register

activate Barista
Barista -> Fridge: 3. Проверить наличие ингредиентов
activate Fridge
Fridge --> Barista: 4. Подтверждение наличия ингредиентов
deactivate Fridge

alt Выбор молока
    Customer -> Barista: 5. Выбрать обычное молоко
    Barista -> Fridge: 6. Достать обычное молоко
    Fridge --> Barista: 7. Обычное молоко
else
    Customer -> Barista: 5. Выбрать альтернативное молоко
    Barista -> Fridge: 6. Достать альтернативное молоко
    Fridge --> Barista: 7. Альтернативное молоко
end

Barista -> WorkStation: 8. Приготовить кофе
activate WorkStation
WorkStation -> Customer: 9. Передать кофе
deactivate WorkStation
deactivate Barista

opt Оставить чаевые
    Customer -> Cash Register: 10. Оставить чаевые
    activate Cash Register
    Cash Register -> Barista: 11. Сообщить о чаевых
    deactivate Cash Register
end
@enduml

В результате мы получим ошибку, потому что ChatGPT в коде у участника Cash Register оставил пробел. Можно попросить исправить. 

Запрос: Исправь ошибку с Cash Register, там не должно быть пробела.

@startuml
autonumber
actor Customer
participant Barista
participant CashRegister as "Cash Register"
participant WorkStation as "Work Station"
participant Fridge as "Fridge"

Customer -> CashRegister: 1. Сделать заказ
activate CashRegister
CashRegister -> Barista: 2. Передать заказ
deactivate CashRegister

activate Barista
Barista -> Fridge: 3. Проверить наличие ингредиентов
activate Fridge
Fridge --> Barista: 4. Подтверждение наличия ингредиентов
deactivate Fridge

alt Выбор молока
    Customer -> Barista: 5. Выбрать обычное молоко
    Barista -> Fridge: 6. Достать обычное молоко
    Fridge --> Barista: 7. Обычное молоко
else
    Customer -> Barista: 5. Выбрать альтернативное молоко
    Barista -> Fridge: 6. Достать альтернативное молоко
    Fridge --> Barista: 7. Альтернативное молоко
end

Barista -> WorkStation: 8. Приготовить кофе
activate WorkStation
WorkStation -> Customer: 9. Передать кофе
deactivate WorkStation
deactivate Barista

opt Оставить чаевые
    Customer -> CashRegister: 10. Оставить чаевые
    activate CashRegister
    CashRegister -> Barista: 11. Сообщить о чаевых
    deactivate CashRegister
end
@enduml

19da57ce3fa4e7e63f5741b00d64bbbd.png

В целом даже похоже на то, что сделала я сама. Большое отличие в том, что для ChatGPT покупатель взаимодействует с кассой сам. Хотя в таком случае логичнее назвать этого участника как-то иначе. 

Забавно, что ChatGPT применил команду autonumber и сам пронумеровал сообщения. 

Заключение

Надеюсь, что эта статья помогла вам в освоении PlantUML. И в понимании того, что PlantUML — лёгкий инструмент. 

Мне кажется, что для первых диаграмм достаточно остановиться на участниках, сообщениях и активациях. И уже по мере освоения добавлять альтернативные сценарии и блоки, а потом и разные украшения. 

Если хотите узнать больше возможностей инструмента, то рекомендую использовать документацию: https://plantuml.com/ru/sequence-diagram. И ещё у них есть неплохой редактор, который сразу будет вам показывать всё то, что вы напишете: http://www.plantuml.com/plantuml/uml/.

© Habrahabr.ru