Неочевидный PlantUML
https://t.me/humane_analystЭто продолжение статьи.
4. Управление вёрсткой
PlantUML предоставляет возможность влиять на расположение элементов, но надо признать, что возможности специфические. Однако здесь есть с чем работать.
4.1. Направление вёрстки
Элементы диаграммы по умолчанию обычно выстраиваются по вертикали сверху вниз. Если попробовать воспользоваться инструкцией left to right direction
, то результат для некоторых видов диаграмм (например, для диаграмм вариантов использования) будет выглядеть лучше.
До:
@startuml "Актор 1" --> (ВИ 1) "Актор 2" --> (ВИ 2) @enduml
После:
@startuml left to right direction "Актор 1" --> (ВИ 1) "Актор 2" --> (ВИ 2) @enduml
Однако, как уже отмечалось в п. 2.2, изменение направления вёрстки приводит к тому, что меняются местами понятия «верх» и «лево», а также «низ» и «право». Также стоит знать следующее.
Существует инструкция top to bottom direction
(сверху вниз), и обычно её использовать не приходится, т.к. это и так значение по умолчанию для большинства поддерживаемых PlantUML диаграмм. Однако для ментальных карт (Mind Map) умолчания другие, и в этом случае инструкция сработает.
@startmindmap skin rose top to bottom direction * Транспортное средство ** Автомобиль ***_ Легковой ***_ Грузовой ** Автобус ** Троллейбус @endmindmap
4.2. Группировка элементов
Идея, заложенная в этом подходе, проста: если объекты на диаграмме сгруппировать, то движок PlantUML будет стараться их расположить на экране ближе друг к другу.
Группировать объекты можно разными способами. Например, можно использовать:
- пакеты — ключевое слово
package
. Так поступают обычно в PDF-версии документации, но с точки зрения синтаксиса языка UML это не всегда уместно; - прямоугольники — ключевое слово
rectangle
. Этот способ выглядит как самый подходящий для группировки вариантов использования («эллипсов») в диаграммах вариантов использования; - специальную возможность — ключевое слово
together
. Этот способ хорош в случаях, когда необходимо избежать появления графических обрамлений вокруг группируемых объектов.
Рассмотрим один из примеров из стандартной документации по PlantUML.
@startuml skin rose left to right direction actor "Food Critic" as fc package Restaurant { usecase "Eat Food" as UC1 usecase "Pay for Food" as UC2 usecase "Drink" as UC3 } fc --> UC1 fc --> UC2 fc --> UC3 @enduml
Если в приведённый пример добавить инструкцию skinparam packageStyle rectangle
, то вместо символа пакета получим рамку системы, как того и требует спецификация UML. Но я лично предпочитаю другой подход: вместо добавления skinparam packageStyle rectangle
достаточно заменить ключевое слово package
на rectangle
. Внешне результат получится таким же, но без дополнительных ухищрений. Да и вообще, рамка, группирующая варианты использования, в UML символизирует систему, а не пакет.
@startuml skin rose left to right direction actor "Food Critic" as fc rectangle Restaurant { usecase "Eat Food" as UC1 usecase "Pay for Food" as UC2 usecase "Drink" as UC3 } fc --> UC1 fc --> UC2 fc --> UC3 @enduml
На самом деле, можно пойти ещё дальше и вообще убрать рамку, воспользовавшись третьим способом. Но лично я предпочитаю рамку оставлять.
@startuml skin rose left to right direction actor "Food Critic" as fc together { usecase "Eat Food" as UC1 usecase "Pay for Food" as UC2 usecase "Drink" as UC3 } fc --> UC1 fc --> UC2 fc --> UC3 @enduml
Небольшое замечание. При рассмотрении первых двух способов всегда использовалось название группирующего элемента (package Restaurant
и rectangle Restaurant
) для обозначения имени моделируемой системы, а в третьем способе его не было (было просто together
). В действительности же PlantUML позволяет убрать название и в первых двух случаях (указав просто package
или rectangle
), а вот добавить название при третьем способе уже не получится.
4.3. Погружение в механику
По умолчанию движок PlantUML пытается расположить все объекты на минимальном пространстве с минимальным числом пересечений линий. Но жизнь сильно разнообразней, чем самый изощрённый алгоритм, поэтому иногда результат расстановки может не отвечать нашим ожиданиям.
Чтобы победить проблему, важно понимать внутреннюю механику, а значит и первопричины того, почему что-то работает так, а не иначе. И тогда уже добиться результата будет проще. Начнём издалека, с математики.
4.3.1. Теория графов
Существует раздел дискретной математики, изучающий специальные виды структур, именуемые графами. Этот раздел называется теорией графов, а для дальнейшего изложения потребуется дать несколько базовых определений.
- Граф — это совокупность вершин (узлов) и рёбер (дуг), соединяющих эти вершины.
- Диграф (ориентированный граф, или попросту орграф) — это граф, состоящий из множества вершин, соединённых направленными рёбрами.
- Взвешенный граф — граф, каждому ребру которого поставлено в соответствие некое значение (вес ребра).
Из этих определений следует, что диаграммы, с которыми приходится иметь дело, с математической точки зрения являются (или могут быть представлены) графами, причём некоторые из них — диграфами. К примеру, классы на диаграмме классов — это вершины, а связи между ними — это рёбра.
Это обстоятельство позволило разработчикам PlantUML задействовать «под капотом» графическую библиотеку GraphViz для отрисовки объектов/элементов своих диаграмм.
4.3.2. Библиотека GraphViz
GraphViz является opensource-решением, позволяющим создавать различные графы с возможностью влиять на расположение вершин и рёбер (более подробно здесь). В GraphViz по умолчанию используется вертикальная вёрстка, но возможно использование и горизонтальной (rankdir=LR
). По своему смыслу оба варианта вёрстки являются взаимоисключающими.
Одним из центральных понятий реализованного в библиотеке подхода является понятие ранга (rank). Ранг присваивается каждой вершине и в зависимости от его величины осуществляется упорядочение этих вершин на экране (условной координатной плоскости). Механика назначения рангов сложна: ранг вершины рассчитывается на основе рангов соседних вершин и весов рёбер, входящих в эту вершину. Детально с алгоритмом можно познакомиться в публикации, но для пояснения логики представленных соображений уже будет достаточно.
Одинаковый ранг у вершин в практическом плане означает, что эти вершины будут расположены на одной линии — на одной горизонтальной линии (условной координате y) при вертикальной вёрстке или на одной вертикальной линии (условной координате x) при горизонтальной вёрстке.
Если ранги вершин различаются, то логика следующая. При вертикальной вёрстке вершины с более высоким рангом располагаются ниже, чем вершины с более низким рангом (т.е. у них значение координаты y больше); при горизонтальной вёрстке вершины с более высоким рангом располагаются правее, чем вершины с более низким рангом (т.е. у них значение координаты x больше).
Для удобства можно использовать следующее правило: чем ближе к началу, тем ниже ранг; чем дальше от начала, тем выше ранг.
В типовой ситуации вершине присваивается более высокий ранг, чем у всех вершин, указывающих рёбрами на него. Пример: если на вершину A3 указывают рёбра, исходящие из вершин A1 и A2, причём у вершины A1 ранг=2, а у вершины A2 ранг=10, то в общем случае вершине A3 будет присвоен ранг больший, чем max {rank(A1), rank(A2)} = max {2, 10} = 10. Это может быть 11, 12 и т.д. Конкретная величина будет определяться ещё рядом других факторов, в частности весом рёбер: чем больший вес присвоен рёбрам, входящим в вершину, тем больший ранг будет присвоен вершине A3.
В соответствии с правилом выше можно утверждать, что самой близкой к началу вершиной будет A1, а самой дальней — A3.
Но здесь есть 2 нюанса:
- библиотека позволяет пользователю указать, какие вершины должны иметь одинаковый между собой ранг (вне зависимости от соседних вершин, весов рёбер, связывающих вершины, и др.), также доступна логическая группировка вершин графа в подграфы; в таком случае ранги остальных вершин потенциально могут быть пересчитаны, чтобы удовлетворить такому «хинту»;
- если несколько вершин имеет одинаковый ранг, то, как отмечалось выше, они обычно выстраиваются в одну строку (или столбец — в зависимости от направления вёрстки). Но это не всё. Вершины по умолчанию выстраиваются в порядке объявления их в коде, при этом GraphViz позволяет вручную переопределить взаимное расположение вершин одного ранга.
4.3.3. PlantUML
Приведённые выше сведения позволяют по-новому взглянуть на поведение PlantUML и привычные нам операции. Имеющиеся у PlantUML возможности и ограничения часто проистекают из использования GraphViz. К примеру, при рассмотрении управления длиной стрелок говорилось, что:
- этот механизм работает только по вертикали или горизонтали (в зависимости от направления вёрстки). А это всё потому, что только одна из двух координат на плоскости в GraphViz используется для ранжирования узлов. Вторая координата содержит объекты одного ранга;
- количество дефисов (или точек, если мы говорим про пунктирную линию) в стрелке/линии позволяет задать длину. Это оттого, что каждый дефис, начиная со второго, на самом деле увеличивает вес ребра графа (стрелки или линии) на единицу (см. здесь). Но это только если он соответствует направлению вёрстки. Вес ребра, в свою очередь, влияет на то, какой ранг присвоить вершине графа (прямоугольнику класса на диаграмме классов, эллипсу варианта использования на диаграмме ВИ и пр.), на которую указывает ребро, а чем ранг выше, тем дальше на диаграмме эта вершина будет расположена.
Важно отметить также, что согласно документации не все диаграммы в PlantUML строятся на основе GraphViz, поэтому в ряде случаев приведённые соображения не будут работать. Так для каких видом диаграмм это точно работает? Список следующий (взято отсюда):
- диаграмма вариантов использования;
- диаграмма классов;
- диаграмма объектов;
- диаграмма компонентов;
- диаграмма развёртывания;
- диаграмма состояний;
- диаграмма деятельности (старая версия).
Означает ли это, что для других диаграмм приведённые соображения вообще не актуальны? Видится, что нет. Причина в том, что мы как потребители оперируем инструкциями PlantUML, которые уже в том или ином случае транслируются в вызовы GraphViz. Но иногда эти инструкции обеспечивают тот же результат, но без обращения к GraphViz. Можно сказать, что мы оперируем контрактом PlantUML, а реализация в каждом конкретном случае может варьироваться.
Так, невидимые стрелки можно успешно применять в диаграммах последовательности и новой диаграмме деятельности, а в п. 4.1 даже был рассмотрен пример, когда направление вёрстки изменялось в Mind Map, притом что ни в одном из названных видов диаграмм GraphViz не используется.
4.3.4. Продвинутые возможности
Вооружившись знаниями о рангах можно рассмотреть конкретные техники. Для этого лучше повторять описанные ниже действия по шагам.
Пример 1 (ориентация сверху вниз).
Предположим, что мы разрабатываем диаграмму классов, и по мере добавления очередного класса PlantUML решает оптимизировать занимаемое диаграммой пространство и переносит один из классов на следующую строку.
@startuml class Первый class Второй @enduml
Поэтому, когда вы добавляете class Третий
расположение классов претерпевает изменение. И так постепенно доходим до 6 классов.
@startuml class Первый class Второй class Третий class Четвёртый class Пятый class Шестой @enduml
Появление второй строки с классами говорит о том, что PlantUML «жонглирует» рангами классов (вершинами графа), чтобы разместить их более компактно на экране. Но мы из своих соображений можем хотеть другое расположение.
Если нам нужно, чтобы «Второй» был левее «Первого», то в простейшем случае будет достаточно перенести объявление class Второй
перед class Первый
(т.к. оба объекта, имеющие один ранг, обычно отрисовываются в порядке их объявления). Но если мы планируем взять расположение классов целиком в свои руки, то придётся поступить следующим образом: указать явно, что «Второй» стоит слева от «Первого»: Первый -left- Второй
.
Если мы хотим, чтобы «Пятый» был ниже «Второго», да ещё и на 2 строки ниже, то надо не просто добавить связь с направлением вниз (-down-
), но и увеличить вес этой связи (ребра графа) на дополнительную единицу (т.е. нужно обеспечить +2 ранга). Это делается через дополнительный дефис: Второй --down- Пятый
. Так как для направлений вниз и вправо есть сокращённая запись, можно было бы написать и так: Второй --- Пятый
.
Предположим, что мы решили расположить «Третий» непосредственно справа от «Второго». Да, благодаря ранее добавленной связи у нас справа от «Второго» уже расположен «Первый», но это ничего. После того, как мы добавляем следующую инструкцию: Второй - Третий
(что равносильно Второй -right- Третий
), мы помещаем «Третий» между «Вторым» и «Первым». Остальные классы на строке подвинутся.
Далее мы можем решить разместить «Шестой» над «Третьим» и связать их отношением зависимости. Для этого понадобится пунктирная линия со стрелкой. Это делается простым добавлением инструкции: Третий .up.> Шестой
. После всех этих действий «Шестой» и «Четвёртый» находятся в самом верху, а «Пятый» в самом низу диаграммы.
Допустим, мы хотим сделать связь «Четвёртого» и «Пятого». Если мы напишем так: Четвёртый --> Пятый
, то это укажет, что «Пятый» должен находиться на 1 строку ниже относительно «Четвёртого» (разница в 1 ранг обеспечивается вторым дефисом). Но у нас «Четвёртый» явно не был ни с кем связан (т.е. его расположение никак не фиксировалось), а «Пятый» ранее был намеренно смещён на 2 строки ниже основной группы классов. В совокупности это приведёт к тому, что «Четвёртый» изменит своё положение, сместившись вниз на 2 строки, дабы оказаться на расстоянии 1 строки от «Пятого». Но если мы не хотим изменения его положения, то достаточно указать опцию[norank]
, которую можно интерпретировать как «исключи эту связь из расчёта рангов, оставь эти объекты в покое». Получаем: Четвёртый --[norank]> Пятый
.
В результате всех манипуляций должно получиться так.
@startuml class Первый class Второй class Третий class Четвёртый class Пятый class Шестой Первый -left- Второй Второй --down- Пятый Второй - Третий Третий .up.> Шестой Четвёртый --[norank]> Пятый @enduml
А теперь задумаемся. Мы ранее управляли взаимным расположением классов, прибегая к указанию связей (линий, стрелок, пунктиров и пр. доступных вариантов). Связи получились искусственными, они могут не отражать тех логических связей, которые мы бы реально хотели представить на диаграмме. В этом случае нам нужно скрыть лишние связи. Для простоты допустим, что лишняя у нас только одна связь, это связь между «Первым» и «Вторым» классами. Значит добавляем к ней опцию [hidden]
и получаем: Первый -left[hidden]- Второй
. После этого линия исчезнет, но расположение классов никак не изменится, чего и требовалось.
Но это ещё не всё. Мы можем захотеть проработать модель более глубоко и указать на диаграмме дополнительные сведения о связях: имя ассоциации и её направление, имена концов ассоциации, их кратность и видимость. PlantUML позволяет добиться и этого, хотя и не без хитростей. Рассмотрим возможный вариант решения.
@startuml class Первый class Второй class Третий class Четвёртый class Пятый class Шестой Первый -left[hidden]- Второй Второй "+Владелец \t1" --down- "+Вещь \t*" Пятый : "Владеет >" Второй "Конец1\n0..1" x-> "+Конец2\n2..*" Третий : "\t\t\t\t" Третий .up.> Шестой Четвёртый --[norank]> Пятый @enduml
Вдаваться в подробности того, что означает тот или иной символ здесь не буду (лучше обратиться к описанию языка UML), но на что хочу обратить внимание. При горизонтальных связях пришлось прибегнуть к символу перевода строки \n
, а при вертикальных связях — к символу табуляции \t
. За счёт этого получилось разрядить представленную информацию, чтобы она не сливалась друг с другом и была визуально различима. Можете попробовать убрать эти символы и оценить изменения.
Подход можно ещё немного улучшить, если использовать инструкции !skinparam nodesep X
и !skinparam ranksep Y
, где X и Y — целые числа. Первая инструкция устанавливает расстояние между вершинами одного ранга, вторая — между вершинами разных рангов. При вертикальной вёрстке (умолчательное значение для диаграммы классов) эти параметры определяют расстояние между графическими изображениями классов по горизонтали (ось x) и вертикали (ось y) соответственно.
Пример 2 (ориентация слева направо).
Пусть мы хотим реализовать систему продажи товаров и для этого прорабатываем назначение проектируемой системы и её границы. В первом приближении мы разработали следующую диаграмму вариантов использования (ВИ).
@startuml skin rose !theme reddress-lightred actor "Посетитель маркетплейса" as visitor actor "Покупатель" as buyer rectangle "Система продажи товаров" { usecase "Просмотр каталога товаров" as UC1 usecase "Изменение содержимого корзины" as UC2 usecase "Оформление заказа" as UC3 usecase "Оплата товаров в корзине" as UC4 } visitor -- UC1 visitor -- UC2 buyer -- UC3 buyer -- UC4 buyer -left-|> visitor @enduml
По форме получилось неплохо, причём даже удалось совместить скин с темой (о такой возможности говорилось в п. 3.1). Но внутреннее чувство прекрасного требует, чтобы диаграмма выглядела канонически: слева — акторы, справа — система. Если попытаться это решить заданием направления связей (для примера: visitor -right- UC1
), то результат будет малопривлекательным. Это, конечно, можно исправить за счёт выстраивания скрытых связей между вариантами использования (чтобы они расположились в столбик), но в этот раз сделаем иначе.
Зададим инструкцию left to right direction
, чтобы принципиально изменить направление вёрстки. В таком варианте границы системы снизу переместятся вправо. И результат, вроде бы, достигнут, но при горизонтальной вёрстке содержимое рамки переворачивается с ног на голову: варианты использования оказываются в нелогичном порядке. Чтобы это исправить, достаточно поменять порядок объявления вариантов использования внутри rectangle
, дабы получилось следующее.
@startuml skin rose !theme reddress-lightred left to right direction actor "Посетитель маркетплейса" as visitor actor "Покупатель" as buyer rectangle "Система продажи товаров" { usecase "Изменение содержимого корзины" as UC2 usecase "Просмотр каталога товаров" as UC1 usecase "Оплата товаров в корзине" as UC4 usecase "Оформление заказа" as UC3 } visitor -- UC1 visitor -- UC2 buyer -- UC3 buyer -- UC4 buyer -left-|> visitor @enduml
Результат достигнут, но на что точно стоит обратить внимание: т.к. мы сменили направление вёрстки, то, как говорилось в п. 4.1, «верх» и «лево», а также «низ» и «право» поменялись местами. Именно поэтому в предпоследней строке осталось указание left
, хотя стрелка направлена вверх. Этот факт поможет следить за мыслью в дальнейшем, а сейчас надо двигаться дальше.
Предположим, что у нас возникла потребность учесть на той же диаграмме регистрацию ранее незарегистрированного пользователя, а также отразить наполнение каталога товаров и отправку сведений о продаже в ФНС. Пойдём по шагам.
После добавления нового варианта использования usecase "Регистрация покупателя" as UC5
все ВИ внутри rectangle
снова перемешиваются. По какой логике это происходит, до конца не понятно, но в такой ситуации лучше вручную расставить варианты использования в том порядке, какой хотелось бы получить по итогу. Мы хотим такой порядок: UC1, UC2, UC5, UC3, UC4.
@startuml skin rose !theme reddress-lightred left to right direction actor "Посетитель маркетплейса" as visitor actor "Покупатель" as buyer rectangle "Система продажи товаров" { usecase "Просмотр каталога товаров" as UC1 usecase "Изменение содержимого корзины" as UC2 usecase "Регистрация покупателя" as UC5 usecase "Оформление заказа" as UC3 usecase "Оплата товаров в корзине" as UC4 } visitor -- UC1 visitor -- UC2 buyer -- UC3 buyer -- UC4 buyer -left-|> visitor @enduml
После перестановки строк диаграмма начинает походить на наши ожидания. Но новый вариант использования UC5 надо ещё связать с другими элементами.
Добавим к коду 2 строки: visitor -- UC5
(чтобы показать, что посетитель может изъявить желание зарегистрироваться прежде, чем начинать покупки) и UC5 .[norank]. UC3 : <<extend>>
(чтобы показать, что при оформлении заказа может выявиться, что пользователь не прошёл регистрацию, и система предложит это исправить). Во втором случае мы используем отношение расширения, поэтому начертание линии сделано пунктирным (для этого вместо дефисов использованы точки), плюс использован стереотип <<extend>>
, как того требует UML.
@startuml skin rose !theme reddress-lightred left to right direction actor "Посетитель маркетплейса" as visitor actor "Покупатель" as buyer rectangle "Система продажи товаров" { usecase "Просмотр каталога товаров" as UC1 usecase "Изменение содержимого корзины" as UC2 usecase "Регистрация покупателя" as UC5 usecase "Оформление заказа" as UC3 usecase "Оплата товаров в корзине" as UC4 } visitor -- UC1 visitor -- UC2 buyer -- UC3 buyer -- UC4 buyer -left-|> visitor visitor -- UC5 UC5 .[norank].> UC3 : <<extend>> @enduml
Стоит сказать, что если бы мы не использовали опцию [norank]
, то UC3 на схеме сдвинулся бы со своего места на одну позицию вправо. Причина этого кроется в рангах:
- UC3 по рангу равен остальным ВИ (поэтому все в одном столбце), но после добавления связи на него начинает указывать UC5;
- эта связь содержит две точки, а все дефисы и точки, начиная со второй, как говорилось в п. 4.3.3, добавляют единицу к весу ребра, который потом прибавится к рангу UC3.
В принципе, можно было бы не отключать ранжирование, результат был бы вполне приличным, но для иллюстрации техники это полезно.
Теперь настала пора добавить акторов «Менеджер» и «ФНС». Первый будет отвечать за наполнение каталога продуктов от лица своей компании (UC6), а второй будет получать сведения о совершённых покупках на маркетплейсе. Регистрацию менеджера и др. детали для простоты рассматривать не будем. Получаем следующий код.
@startuml skin rose !theme reddress-lightred left to right direction actor "Посетитель маркетплейса" as visitor actor "Покупатель" as buyer actor "Менеджер" as manager actor "ФНС" as tax rectangle "Система продажи товаров" { usecase "Просмотр каталога товаров" as UC1 usecase "Изменение содержимого корзины" as UC2 usecase "Регистрация покупателя" as UC5 usecase "Наполнение каталога товаров" as UC6 usecase "Оформление заказа" as UC3 usecase "Оплата товаров в корзине" as UC4 } visitor -- UC1 visitor -- UC2 visitor -- UC5 buyer -- UC3 buyer -- UC4 buyer -left-|> visitor UC5 .[norank].> UC3 : <<extend>> UC4 ---> tax UC2 -[hidden]- UC6 UC6 -- manager @enduml
Так как мне захотелось расположить новый ВИ UC6 справа от UC2, то я добавил скрытую связь именно с UC2. Это позволяет продемонстрировать, что располагать варианты использования можно в нескольких столбцах и на разных позициях по вертикали.
Плюс двух новых акторов я для красоты разместил справа от рамки системы. Чтобы это стало возможным, выполнено следующее:
- акторы размещены справа от связей с соответствующими вариантами использования (
UC4 ---> tax
иUC6 -- manager
); - для связи с ФНС добавлен дополнительный дефис (итого их стало 3). Причина в том, что при двух дефисах актор tax попал бы в тот же столбец, что и UC6 (ранг UC4 + 1 ранг от веса связи с двумя дефисами с актором tax это то же самое, что и ранг UC2 + 1 ранг от веса скрытой связи с UC6).
При желании можно продолжать насыщать диаграмму деталями. Давайте рассмотрим, как можно при описании отношения расширения ВИ указать точку расширения (не самая часто используемая на практике возможность).
Сперва следует добавить в расширяемый вариант использования (UC3) точку, а затем прикрепить примечание (note), на котором указано условие расширения в фигурных скобках.
Последняя версия спецификация UML предписывает прикреплять примечание к связи между ВИ (в нашем примере это связь между UC5 и UC3) с помощью пунктирной линии. У нас же вышло несколько иначе.
@startuml skin rose !theme reddress-lightred left to right direction actor "Посетитель маркетплейса" as visitor actor "Покупатель" as buyer actor "Менеджер" as manager actor "ФНС" as tax rectangle "Система продажи товаров" { usecase "Просмотр каталога товаров" as UC1 usecase "Изменение содержимого корзины" as UC2 usecase "Регистрация покупателя" as UC5 usecase "Наполнение каталога товаров" as UC6 usecase "Оформление заказа\n--\n<b>extension points</b>\nВыбор покупателя" as UC3 usecase "Оплата товаров в корзине" as UC4 } visitor -- UC1 visitor -- UC2 visitor -- UC5 buyer -- UC3 buyer -- UC4 buyer -left-|> visitor UC5 .[norank].> UC3 : <<extend>> note bottom on link : "condition: {посетитель не зарегистрирован в системе}\nextension point: Выбор покупателя" UC4 ---> tax UC2 -[hidden]- UC6 UC6 -- manager @enduml
Как можно заметить, PlantUML помещает примечание непосредственно на связь (графическое изображение отношения), а не прикрепляет его пунктирной линией. Возможно, получилась не самая высокохудожественная композиция, но подход вполне рабочий.
Если такой стиль показался вам не по вкусу, то могу предложить другой способ, который, впрочем, тоже не совсем соответствует спецификации UML. Можно связать примечание с обоими ВИ.
@startuml skin rose !theme reddress-lightred left to right direction actor "Посетитель маркетплейса" as visitor actor "Покупатель" as buyer actor "Менеджер" as manager actor "ФНС" as tax rectangle "Система продажи товаров" { usecase "Просмотр каталога товаров" as UC1 usecase "Изменение содержимого корзины" as UC2 usecase "Регистрация покупателя" as UC5 usecase "Наполнение каталога товаров" as UC6 usecase "Оформление заказа\n--\n<b>extension points</b>\nВыбор покупателя" as UC3 usecase "Оплата товаров в корзине" as UC4 note "condition: {посетитель не зарегистрирован в системе}\nextension point: Выбор покупателя" as note1 } visitor -- UC1 visitor -- UC2 visitor -- UC5 buyer -- UC3 buyer -- UC4 buyer -left-|> visitor UC5 .[norank].> UC3 : <<extend>> UC4 ---> tax UC2 -[hidden]- UC6 UC6 -- manager UC5 .. note1 UC3 .. note1 @enduml
Какой из двух вариантов предпочесть, оставлю на ваше усмотрение. И на этом предлагаю пример считать законченным.