Неочевидный PlantUML

Неочевидный PlantUML

https://t.me/humane_analyst



Это продолжение статьи.

Страницы: 1 2 3 4


5. О диаграммах последовательности

В п. 2.1 разбирались стрелки, используемые в диаграммах последовательности. Это было важно для дальнейшего представления стилей связей и темы управления вёрсткой (раздел 4).

Теперь настало время поговорить про сами «сиквенсы».


5.1. Ответные сообщения

Если посмотреть документацию по PlantUML, то можно прочитать, что для рисования стрелки ответного сообщения можно использовать функцию return. И в документации приводится следующий пример.

@startuml
Bob -> Alice : hello
activate Alice
Alice -> Alice : some action
return bye
@enduml


Функция return действительно работает, она даже автоматически деактивирует линию жизни. Казалось бы, очень удобно, но я настоятельно не рекомендую её использовать сразу по трём причинам. 

Во-первых, как отмечал в п. 2.1, спецификация UML предписывает иной тип острия стрелки для ответных сообщений, и return не позволит добиться желаемого. Добавление инструкции skinparam style strictuml тоже не поможет.

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

В-третьих, и это уже более существенная проблема, принцип работы return основан на правиле: ответная стрелка проводится до того объекта, который последним активировал линию жизни вызванного объекта. И вот тут на практике и возникают проблемы. Если вы забудете активацию линии жизни (или просто не любите этого делать), то return укажет не туда. В этом вы можете убедиться, если в примере выше удалите строку activate Alice

Но даже если вы всё делаете аккуратно и перепроверяете за собой, то в один прекрасный момент вам может потребоваться внести изменение куда-нибудь в середину давно существующей диаграммы, и всё «поплывёт». Поводом может стать асинхронное взаимодействие, изображение широковещательной рассылки или даже обычный фрейм (он же фрагмент) вроде alt или opt.

Более того, return может приводить к сбою, если линия жизни вызывающего объекта (в примере выше это Bob) не была активирована. Попробуйте проверить работу следующего примера, а после этого поочерёдно расскоментируйте строки с return и закомментируйте соответствующие строки с явным указанием возврата.

@startuml
skinparam style strictuml
Bob -> Alice : hello
activate Alice
alt Условие
   Alice -->> Bob : Хороший ответ
   'return Хороший ответ
else Иначе
   Alice -->> Bob : Плохой ответ
   'return "Плохой ответ"
end
@enduml


Явные стрелки и return на элементарном примере


В первом случае всё выглядит адекватно. Во втором случае (когда вместо первой стрелки внутри фрейма используется return) остриё стрелок ответных сообщений перестаёт выглядеть одинаково, а также линия жизни прерывается, не дойдя до альтернативной ветки во фрейме, что неверно. В третьем случае (когда вместо обеих стрелок внутри фрейма используется return) возникает ошибка, говорящая о том, что возвращаться просто некуда.

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

5.2. Нумерация шагов

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

5.2.1. Настройка нумерации

Инструкция по автонумерации шагов выполняется в соответствии с маской: autonumber [start [increment]] [format], где квадратные скобки обозначают необязательный элемент маски.

start отвечает за номер, который будет присвоен ближайшей следующей стрелке. Может принимать любое неотрицательное значение, умолчательное значение — 1. Вместе с этим стоит учитывать следующие особенности: 

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

increment отвечает за шаг, с которым будет изменяться счётчик (т.е. это величина приращения номера для каждой последующей стрелки). Может принимать любое неотрицательное значение, умолчательное значение — 1. Если указать значение 0, то это приведёт к тому, что все последующие стрелки получат одинаковый номер (поскольку добавление нуля не приводит к изменению числа).

format отвечает за форматирование номера. Здесь применима разметка на языке HTML, и примеры можно посмотреть в документации к «планту». Однако я хочу предостеречь от стиля написания, используемого в документации. В ней очень часто не закрывают теги. Может показаться, что это не важно, но на практике это приводит к непредсказуемым эффектам. В одних случаях начертание символов будет зависеть от числа использованных пробелов в маске (какая тут связь — загадка), в других случаях стиль автонумерации при рендеринге может примениться к тексту сообщения на стрелке (и текст окрасится в красный, к примеру). Лечится это довольно просто: надо закрыть теги. К примеру, вместо "<font color=red><b>##" стоит написать "<font color=red><b>##</b></font>". Справедливости ради стоит сказать, что в соответствии со стандартом HTML следовало бы заключить значение атрибута в кавычки, но, раз уж двойные кавычки уже задействованы в синтаксисе PlantUML, допускается использование одинарных (т.е. получаем: "<font color='red'><b>##</b></font>"). Но работать будет и без них.


@startuml
autonumber 10 "<font color='red'><b>##</b></font>"
Bob -> Alice : hello
Alice --> Bob : hello!
@enduml
Демонстрация правильного форматирования для нумерации


Отдельно стоит упомянуть возможность создания двух- и трёхуровневой нумерации. Это позволяет создавать номера вида 1.2 и 5.3.1. Пример объявления: autonumber 1.1.1

Особенность работы с такого рода нумерацией состоит в том, что автоматически инкрементироваться будет только последняя цифра: 2-я для двухуровневой нумерации и 3-я — для трёхровневой. Для того, чтобы инкрементировать первую цифру, нужно явно вызвать инструкцию autonumber inc A. А для инкрементирования второй цифры трёхуровневой нумерации нужно явно вызвать autonumber inc B.

Ниже приведён пример из документации.

@startuml
autonumber 1.1.1
Alice -> Bob: Authentication request
Bob --> Alice: Response

autonumber inc A
'Now we have 2.1.1
Alice -> Bob: Another authentication request
Bob --> Alice: Response

autonumber inc B
'Now we have 2.2.1
Alice -> Bob: Another authentication request
Bob --> Alice: Response

autonumber inc A
'Now we have 3.1.1
Alice -> Bob: Another authentication request
autonumber inc B
'Now we have 3.2.1
Bob --> Alice: Response
@enduml
Использование стандартной трёхуровневой нумерации


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

@startuml
' Шаг автонумерации(2-й параметр)
' влияет только на 3-ю цифру нумерации
autonumber 1.1.5 2
Bob -> Alice : hello
Alice --> Bob : hello!

' Увеличиваем сразу 2 цифры номера,
' после чего 3-я цифра сбросится в начало
autonumber inc A
autonumber inc B
Bob -> Alice : bye!
Alice --> Bob : bye-bye!
@enduml
Использование автонумерации с шагом 2


Нюанс: не стоит вызывать autonumber inc B для двухуровневых номеров. Последняя цифра (т.е. 2-я) должна инкрементироваться автоматически, а первая цифра — путём вызова autonumber inc A. Вызов autonumber inc B в таких ситуациях хоть к ошибке и не приведёт, но последствия могут удивить, к примеру, «поплывёт» форматирование текста на диаграмме.

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

@startuml
' Шаг автонумерации(2-й параметр)
' влияет на единственную цифру нумерации (1-й параметр)
autonumber 5 2 "<font color=red><b>'1.1.'##</b></font>"

Bob -> Alice : hello
Alice --> Bob : hello!

' Изменяем вручную префикс (2 цифры в одинарных кавычках),
' а счётчик сбрасываем в начало (1-й параметр)
autonumber 1 2 "<font color=red><b>'2.2.'##</b></font>"
Bob -> Alice : bye!
Alice --> Bob : bye-bye!
@enduml
Эмуляция трёхуровневой нумерации


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

Вместе с этим у данного подхода есть и неочевидное преимущество. Добавляемый вручную префикс считается частью номера, в этом можно убедиться, если обратитесь к значению переменной %autonumber% в коде. Примеры такого обращения будут представлены ниже при рассмотрении примечаний.


5.2.2. Сохранение существующей нумерации

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

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


Способ 1. Использование шага нумерации.

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

Программа на BASIC и ничего лишнего


Аналогичный подход можно реализовать и для диаграмм последовательности. Достаточно в начало кода вставить инструкцию для автонумерации стрелок с шагом в 10. В случае же, когда потребуется вставить новые стрелки между двумя существующими (в примере ниже мы вставляем стрелки между стрелками с номерами 20 и 30), достаточно выполнить следующее: 

  • добавить перед второй стрелкой новую инструкцию autonumber с фиксацией номера, который уже сложился для этой стрелки (для нашего примера это 30);
  • между строкой с первой стрелкой и добавленной на предыдущем шаге вставить строку с инструкцией autonumber с указанием нужного номера и далее добавить строки с нужными новыми стрелками.

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

@startuml
' Позволит при вставке стрелок между существующими использовать тот же формат
!$autonum_format = "<font color=red><b>##</b></font>'.'"

autonumber 10 10 "$autonum_format"
a -> b : 1-я старая стрелка
b -> c : 2-я старая стрелка

' Раскомментировать следующие 3 строки в 3-ю очередь
'autonumber 22 "$autonum_format"
'c -> d : 2-я новая стрелка
'd --> c : 3-я новая стрелка

' Раскомментировать следующие 2 строки во 2-ю очередь
'autonumber 25 "$autonum_format"
'c -> c : 1-я новая стрелка

' Раскомментировать следующую строку в 1-ю очередь
'autonumber 30 10 "$autonum_format"
c --> b : 3-я старая стрелка
b --> a : 4-я старая стрелка
@enduml
Подход к нумерации стрелок сообщений в стиле BASIC


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

И ещё пара замечаний.

  • Поскольку рассмотренная техника предполагает возможность использования одного и того же формата для нумерации, я ввёл в рассмотрение переменную $autonum_format, единожды её проинициализировал и далее использовал в каждой инструкции autonumber. Такой подход избавляет от возможных ошибок и позволяет за один раз изменить формат, если в том возникнет потребность.
  • При задании формата я для красоты добавил точку, которая отделяет номер стрелки от текста, причём точка вынесена за закрывающие теги и взята в одинарные кавычки. Это позволило добиться того, чтобы красное жирное начертание, используемое для цифр номера, на неё не распространялось.


Способ 2. Использование подпунктов.

Если идея пропуска номеров, представленная выше, не нравится, можно поступить иначе: при вставке строк можно присваивать им значения в виде подпунктов. Так, вставляя стрелку между стрелками 2 и 3, новой стрелке присваивается номер 2.1; если нужно вставить несколько стрелок, то им последовательно присваиваются номера 2.1, 2.2, 2.3 и т.д.

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

@startuml
autonumber 1 1
a -> b : 1-я старая стрелка
b -> c : 2-я старая стрелка

' Раскомментировать следующие 3 строки в 3-ю очередь
' и закомментировать autonumber из 2-й очереди
'autonumber 2.1
'c -> d : 2-я новая стрелка
'd --> c : 3-я новая стрелка

' Раскомментировать следующие 2 строки во 2-ю очередь
'autonumber 2.1
'c -> c : 1-я новая стрелка

' Раскомментировать следующую строку в 1-ю очередь
'autonumber 3 1
c --> b : 3-я старая стрелка
b --> a : 4-я старая стрелка
@enduml
Подход к нумерации стрелок сообщений в стиле подпунктов


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


5.3. Примечания

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

@startuml
skinparam participantPadding 100
a -> b
note left #LemonChiffon : Слева от стрелки
b --> a
note right #LemonChiffon : Справа от стрелки
a -> b 
note left #LightCyan
   Слева от стрелки, 
   но на несколько строк
end note
b --> a
note right #LightCyan : Справа от стрелки,\nно на несколько строк благодаря \\n
note left of a #HoneyDew : Слева от линии жизни 'a'
note right of b #HoneyDew : Справа от линии жизни 'b'
note left of b #HoneyDew : Слева от линии жизни 'b'
note right of a #HoneyDew : Справа от линии жизни 'a'
note over a #LightSkyBlue : Поверх 'a'
note over a, b #LightSkyBlue : Поверх 'a' и 'b'
note over a #Ivory : Поверх 'a' и вместе с этим...
/ note over b #Ivory : ...на той же строке поверх 'b'
note across #Linen : Поверх всех линий жизни, сколько бы их ни было
@enduml
Основные способы задания примечаний


В данном примере разные группы примечаний для наглядности окрашены в разные цвета. Если потребности в такой раскраске нет, можно удалить соответствующие инструкции (символ решётки и следующие за ним символы с кодом цвета). Дополнительно в примере задействована инструкция skinparam participantPadding 100, которая позволила явно отдалить линии жизни объектов-участников друг от друга.

Но это не всё. PlantUML позволяет изменить графическую форму примечания, если вместо note использовать одно из указанных ключевых слов:

  • hnote — придать примечанию форму шестиугольника;
  • rnote — придать примечанию форму прямоугольника.

Про стандартные примечания поговорили, но хочется остановиться ещё на одном аспекте. На практике может возникнуть потребность снабдить примечанием не отдельную стрелку, а сразу группу из нескольких стрелок. Штатно такой возможности в PlantUML нет, но её можно сэмулировать, если сгруппировать стрелки (group) и к ним задать примечание. В первом приближении решение выглядит следующим образом.

@startuml 
a -> b
b --> a
group \t
   a -> b
   b -> c
end
note right
   Этот комментарий прикреплён к группе, и он может
   содержать много строк, поясняя происходящее
end note
c --> b
b --> a
@enduml
Примечание к группе (наивная реализация)


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

Чтобы избежать таких накладок можно прибегнуть к созданию скрытых стрелок, которые заняли бы место на диаграмме, отодвинув существующие стрелки вниз. Но как быть, если мы захотим пронумеровать стрелки? Каждой скрытой стрелке будет всё равно присвоен свой номер, а значит визуально получится «дыра» в нумерации. 

Чтобы избежать такого эффекта, можно прибегнуть к специальной форме уже знакомой инструкции autonumber. Она позволяет остановить счётчик, а потом продолжить его. Полученный результат представлен ниже.

@startuml
!$autonum_format = "<font color=navy>##</font>'.'"
autonumber "$autonum_format"
a -> b
b --> a
note right
   Для меня <U+0025>autonumber<U+0025> = %autonumber%
end note
' Если название группы не требуется, можно вместо него написать 
' символ табуляции, как в предыдущем примере
group Группа
   a -> b
   b -> c
end
note right
   Этот комментарий прикреплён к группе,
   и это позволяет ему быть очень длинным
   и комментировать сразу несколько стрелок за раз.
   
   Но тут надо иметь в виду, что комментарий добавляется
   после всех стрелок в группе. 
   
   Для меня <U+0025>autonumber<U+0025> = %autonumber% (от последней стрелки)
end note
' Останавливаем работу счётчика стрелок
autonumber stop
' 2 скрытые стрелки (их количество определяется ситуативно)
c -[hidden]-> c
c -[hidden]-> c
' Возобновляем работу счётчика стрелок
autonumber resume
c --> b
b --> a
@enduml
Примечание к группе (более изощрённая реализация)


Однако данное решение можно улучшить. Достаточно вместо скрытых стрелок использовать указание явных промежутков (отступов). Так, если после большого примечания написать ||50|, то будет добавлен промежуток в 50 пикселей; это достаточно, чтобы получить нужный результат, а кроме того, не потребуется останавливать и возобновлять работу счётчика. Получаем следующее. 

@startuml
!$autonum_format = "<font color=navy>##</font>'.'"
autonumber "$autonum_format"
a -> b
b --> a
note right
   Для меня <U+0025>autonumber<U+0025> = %autonumber%
end note
' Если название группы не требуется, можно вместо него написать 
' символ табуляции, как в предыдущем примере
group Группа
   a -> b
   b -> c
end
note right
   Этот комментарий прикреплён к группе,
   и это позволяет ему быть очень длинным
   и комментировать сразу несколько стрелок за раз.
   
   Но тут надо иметь в виду, что комментарий добавляется
   после всех стрелок в группе.
   
   Для меня <U+0025>autonumber<U+0025> = %autonumber% (от последней стрелки)
end note
' Можно также пропустить строки, вставляя по 3 вертикальных черты (|||)
||50|
c --> b
b --> a
@enduml


Визуально результат будет таким же, как при предыдущей реализации.


5.4. Управление представлением

В PlantUML предусмотрено много различных возможностей повлиять на визуальное представление диаграммы. В данном пункте будут представлены наиболее полезные приёмы с точки зрения их влияния на структуру диаграммы; цветовая окраска будет использоваться в минимальном объёме.

Весомый вклад в формирование внешнего вида вносят инструкции skinparam.

  • minClassWidth X — минимальная ширина графического элемента, представляющего участника взаимодействия (человечек, прямоугольник и др.) на линии жизни. X — значение в пикселях.
  • boxPadding X — величина отступа между блоками участников, сгруппированных с помощью box. X — значение в пикселях.
  • maxMessageSize X — максимально допустимый размер одной строки текста сообщения, размещённого на стрелке. Все символы, выходящие за эту величину будут автоматически переноситься на следующую строку. X — значение в пикселях.
  • participantPadding X — величина отступа между отдельными участниками. X — значение в пикселях.
  • sequenceMessageAlignment A — способ выравнивания текста сообщения, размещённого на стрелке. A — одно из следующих значений: left — выравнивание по левой стороне (значение по умолчанию), center — выравнивание по центру, right — выравнивание по правой стороне.
  • responseMessageBelowArrow B — способ размещения текста ответного сообщения. B — одно из следующих значений: false — текст размещается строго над стрелкой (значение по умолчанию), true — текст размещается под стрелкой. Данная инструкция в настоящий момент работает только в случае, когда стрелка указывает влево (см. текст примера ниже).

Также полезно сразу в явном виде указывать порядок участников. Это позволит избежать ситуации, когда PlantUML попробует переставить участников на своё усмотрение, испортив вашу задумку. Кроме того, для упорядочения участников будет разумным использовать шаг 10. Причины этого аналогичны тем, что были приведены выше при рассмотрении нумерации сообщений (стрелок) в стиле BASIC.

Про остальные моменты можно почитать в комментариях из кода ниже. Также рекомендую поочерёдно закомментировать различные инструкции, чтобы оценить их влияние.

@startuml
skinparam {
   minClassWidth 50
   boxPadding 25
   maxMessageSize 260
   participantPadding 60
   sequenceMessageAlignment center
   responseMessageBelowArrow true
}
' Объединение в box'ы позволяет показать, что группируемые элементы
' связаны в каком-то отношении. Это могут быть модули одной системы, системы, размещённые в одном сегменте сети, и пр.
box Пользователи #aliceBlue
' Нумерацию участников стоит закладывать сразу и нумеровать с шагом 10.
' Это позволит легко вставлять новых участников и передвигать существующих.
   actor Клиент as a order 10
end box

box Системы #snow
   participant b order 20
   participant c order 30
end box
box Другие системы #mintCream
   participant d order 40
end box

== 1. Стоит использовать ==
a -> b : Текст сообщения будет перенесён автоматически (maxMessageSize)
b -->> a : и выровнен по центру (sequenceMessageAlignment).
' Пропуск ||| для отделения новой цепочки вызовов. Это улучшает восприятие 
|||
a -> c : Линии жизни отодвинуты друг от друга (participantPadding).
c -->> a : Но на длинных стрелках автопереносы текста могут выглядеть неуместными, ведь с краёв остаётся много пустоты
|||
a -> d :  Группы участников (box) отодвинуты друг от друга (boxPadding),
d -->> a : плюс прямогуольники для b, c и d сделаны шире (minClassWidth)
|||
' Разделители == позволяют на большой диаграмме выделить относительно независимые смысловые блоки. Это улучшает восприятие

== 2. На любителя ==
a -> c : Текст ответного сообщения можно разместить снизу (responseMessageBelowArrow),
a <<-- c : но только если направить стрелку справа налево
@enduml
Иллюстрация различных приёмов, влияющих на представление



Страницы: 1 2 3 4

Report Page