Я дико люблю ковыряться в чужом коде. Это одна из моих любимых специализаций. То есть я просто беру чужой код, анализирую его, читаю. Как я читал его раньше: я переводил код в русский язык. Описывал, что происходит по флоу кода, и пытался понять, что там происходит. Эти записи я в дальнейшем использовал как для написания статей в Confluence, так и для общего понимания происходящего.

С одной стороны, решение работающее. С другой, буквально через неделю-две я уже начинал сомневаться, достаточно точно ли я «перевел» с кода на русский язык? И тогда вспомнил про UML-диаграммы. И вместо того, чтобы записывать текст, стал визуализировать его и исписал неимоверное количество тетрадей. 

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

Давайте вспомним, что такое Unified Modeling Language. Чаще всего в университете UML используется для описания диаграммы классов.

У человека есть паспорт, у дома есть стены. У класса «дерево» есть птицы, дерево умеет сбрасывать листья. Birds — это интерфейс, который реализует fly, и есть некий Raven, который его реализует. Чтобы написать код, который выдаст схему, мне потребовалось минуты две. Их я потратил на то, чтобы придумать, что нарисовать.

@startjson
{
  "payment":{
    "paymentId":"804900",
    "type":"PAYMENT",
    "createdDateTime":"2020-11-28T12:58:49+03:00",
    "status":{
        "value":"SUCCESS",
        "changedDateTime":"2020-11-28T12:58:53+03:00"
    },
    "amount":{
      "value":100.00,
      "currency":"RUB"
    },
    "paymentMethod":"..",
    "customer":"..",
    "gatewayData":"..",
    "billId":"autogenerated-a51d0d2c-6c50-405d-9305-bf1c13a5aecd",
    "flags":[]
  },
  "type":"PAYMENT",
  "version":"1"
}

@endjson

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

Для чего я использую PlantUML в работе

Во-первых, это анализ чужого кода. Я разработчик процессинга Qiwi Кошелька, и у нас есть код, который занимается OAUTH2  авторизацией. Я читал его уже раз восемь, но через пару недель все равно забываю, как он работает. При том, что код лаконичный, динамичный, он позволяет делать, что угодно. Но каждый раз, когда я открываю классы, и вижу OAuthCore класс, думаю: “е-мое”. Когда я нарисовал схему, мне и моим коллегам стало гораздо проще ковырять этот код.

Во-вторых, для объяснения, как что-то работает. С наступлением пандемии, когда все ушли на удаленку, для описания задач и фичей мы использовали notepad, textedit, sublime - все, что попадало под руку, чтоб зафиксировать текст. Все это предсказуемо терялось, договоренности забывались. Преодолевать споры не помогали и письма в рабочую почту. Тогда я предложил своей команде использовать PlantUML для описания фичей, а именно стандартные Sequence и State диаграммы, которые позволяют сразу привести решение в порядок.

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

Давайте посмотрим, как работает PlantUML на примере из продуктовой разработки.

Кейсы

Допустим, мы создаем маркетплейс — продукт QMarket. 

  • Каждый пользователь может завести деньги на счет в системе

  • Каждый пользователь может выставлять товар в системе

  • Система позволяет продавать, используя арбитраж

  • Пользователи свободно могут выводить средства на карту

В него можно заводить и выводить деньги, есть какой-то счет. В общем, стандартный пример, ничего нового. И есть команда, которая занимается процессингом платежей. Наша задача: подготовить флоу по отправке запросов на выплату.

Вместо того, чтобы описывать задачу текстом, нарисуем диаграмму.

Участник — SPA (Single Page Application), в котором есть пользователи. SPA отправляет запрос. Блок онлайн процессинга создает Txn и отправляет ее в базу, база возвращает новую транзакцию в статусе Initial.

В процессинге одна из самых частых проблем — как сделать так, чтобы один и тот же платеж не проводился дважды. Для этого используется идемпотентность. В запросе client payment request есть ключ идемпотентности. На схеме мы показываем, что если транзакция с таким Request ID уже есть, мы проверяем поля. Модуль онлайн процессинга проверяет идемпотентность. Если поля транзакции, которые должны совпадать, совпадают, то все хорошо, идем дальше. Если нет, выдаем ошибку, что запрос с таким Request ID существует.

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

Когда вопрос с лимитами разрешился, мы обновляем статус транзакции, отправляем One Time Password (OTP). SPA возвращает нам Confirmation ID, по которому рендерится форма ввода OTP. Мы обновляем статус на Waiting For SMS Confirmation и отправляем запрос. 

На описание этой схемы я потратил больше времени, чем на ее непосредственную реализацию на PlantUML.

@startuml
'https://plantuml.com/sequence-diagram

autonumber

title Создание запроса на выплату
actor spa
participant "Online processing" as online
database "Market Database" as database
participant "Limits Service" as limits
participant "SMS Service" as sms

spa -> online: CreatePaymentRequest
online -> db: CreateTxn
db -> online: Новая транзакция в статусе INITIAL
alt Транзакция с таким requestId уже есть, проверяем поля
online -> online: check idempotency
return Транзакция
else Поля транзакций и запроса не совпадают
online --> spa: Ошибка, запрос уже существует
end alt
online -> limits: Можно ли провести txn
return ok
alt Лимиты превышены
return fail
online -> db: Обновить статус транзакции в LIMITS_OVERFLOW
return ok
online --> spa: Ошибка, превышены лимиты
end alt
online -> db: Обновить статус транзакции в LIMITS_OK
return ok
online -> sms: Отправить OTP пользователю
sms --> online: ConfirmationID
online -> db: Обновить статус транзакции в WAITING_SMS_CONFIRMATION
online --> spa: CreatePaymentResponse с confirmationId

@enduml

С title все понятно, это заголовок диаграммы. Дальше идет описание участников. Actor spa это наш пользователь, participant — участник диаграммы — Market Database, Limits Service и SMS Service. Все остальное — отношения между этими сущностями. 

Дальше — больше. Можно расписать статусы транзакций по флоу выплаты платежа. Для этого подойдет State диаграмма.

Платеж уже создан, и есть два кейса: лимиты превышены или лимиты пройдены. Если лимиты превышены, мы идем в самый конец, в точку, где платеж не удалось выполнить. Если лимиты пройдены, то проверяем смс-подтверждение, получаем от пользователя OTP, и затем можно отправлять статус Ready To Sent в платежную систему.

В платежной системе мы пытаемся создать платеж, и здесь тоже два кейса: платеж отклонен платежной системой или отправлен. После мы отправляем запрос в платежную систему о статусе, и ставим статус “платеж выплачен”. 

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

@startuml
'https://plantuml.com/state-diagram
title Выплата платежа - статусы транзакций


scale 350 width

INITIAL: Платеж создан
LIMITS_EXCEED : Лимиты превышены
LIMITS_OK : Лимиты пройдены
WAITING_SMS_CONFIRMATION: Ожидание OTP
SENT_TO_PAYSYSTEM: Отправлен в платежную систему
PAID: Выплачен
READY: Платеж готов к отправке в платежную систему
DECLINED_BY_PAYSYSTEM: Платеж отклонен платежной системой

[*] -> INITIAL
INITIAL -right-> LIMITS_EXCEED
LIMITS_EXCEED -> [*]: Платеж не удалось выполнить

INITIAL -down-> LIMITS_OK: Лимиты пройдены
LIMITS_OK -down-> WAITING_SMS_CONFIRMATION: Ожидаем sms подтверждение
WAITING_SMS_CONFIRMATION -down-> READY: Можно отправлять в платежную систему
READY -down-> SENT_TO_PAYSYSTEM: Платеж зарегистирован в платежной системе
READY -down-> DECLINED_BY_PAYSYSTEM: Платеж отклонен
SENT_TO_PAYSYSTEM -down-> PAID
DECLINED_BY_PAYSYSTEM -down-> [*]: Платеж отклонен
PAID -> [*]: Платеж выполнен

@enduml

И еще пример разбора кода. Он менялся от ревью к ревью, и в него закралась ошибка. 

package com.qiwi.qsp6.marketplace.payout.workflow

import com.qiwi.qsp6.marketplace.payment.model.Payment
import com.qiwi.qsp6.marketplace.payment.model.PaymentStatus
import com.qiwi.qsp6.marketplace.payout.client.PayoutClient
import com.qiwi.qsp6.marketplace.payout.model.PayoutRequest
import com.qiwi.qsp6.marketplace.payout.model.toPayoutRequest
import com.qiwi.qsp6.marketplace.service.PaymentService

class PayoutWorkflow(val paymentService: PaymentService, val payoutClient: PayoutClient) {
    fun sentToPaySystem(payment: Payment) {
        try {
            val payoutRequest = PayoutRequest(payment.id, payment.externalId)
            // .. todo a lot of business logic for validation
            paymentService.updateStatus(PaymentStatus.SENT_TO_PAYSYSTEM)
            // ..
            // ..
            val response = payoutClient.sendClient(payment.toPayoutRequest())
        } catch (e: Exception) {
            // TODO: Some business aware exception and logging
        }
    }
}

Посмотрим на этот код в виде диаграммы.

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

@startuml
'https://plantuml.com/component-diagram
title Поиск ошибки в отправке платежа

participant "Offline processing" as offline
database "Market DB" as db
participant "Payment System" as ps

offline <- db: Get txn to process
offline -> db: Update status to SENT_TO_PAYSYSTEM
offline -> ps: Отправка платежа
return ok

@enduml

Модель С4

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

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

Пример диаграммы системы
Пример диаграммы системы

На что здесь обратить внимание? Пользователи, основные участники, основаны на абстракциях. Кроме названия, самого actor, есть еще описание, что он из себя представляет. Дальше видно, что контейнер “процессинг платежей” выделен синим. Появилась легенда: внешние системы, внешние пользователи, система. Эту диаграмму тоже можно отдавать в эксплуатацию.

@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml

LAYOUT_WITH_LEGEND()
title Диаграмма контекста (1 уровень)

Person(customer, "Пользователи", "Пользователи нашей системы, как продавцы так и покупатели")
System(processing, "Процессинг платежей", "Принимает запросы от пользователей для проведения платежей и проводит их")

System_Ext(notifications, "Сервис нотификации", "Сервис, способный отправлять сообщения пользователям по sms/e-mail")
System_Ext(backoffice, "Система бекоффиса", "Отвечает за учет всех платежей, обмен реестрами с платежными системами")
System_Ext(paysystems, "Платежные системы", "Qiwi wallet, эквайринг и прочие платежные системы")

System_Ext(other, "Другие части системы", "Включают в себя сайт, торговые площадки, и все остальное, что не касается конкретно нашей команды")

Rel(customer, processing, "Создают платежи")
Rel_Back(customer, notifications, "Отправляют пользователям уведомления")
Rel_Neighbor(processing, notifications, "Отправляют уведомления", "HTTP")
Rel(backoffice, processing, "Учет платежей")
Rel(processing, paysystems, "Отправляют запросы на пополнения")
Rel(backoffice, paysystems, "Обмен реестрами, денежными средствами и т.д.")
Rel(customer, other, "Используют")

@enduml

Берем набор шаблонов с Гитхаба. Можно использовать файлики, здесь ничего нового. Выбираем layout и legend. Обратите внимание, что если раньше мы использовали просто слова, здесь уже идут макросы, которые похожи на классы. Есть person, который мы назвали customer, и его описание. Система — processing. Внешние системы — это notifications, backoffice, paysystems и другие. После определения всех участников диаграммы мы продолжаем рассказывать про связи.

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

В ней есть контейнер Online, который занимается онлайн приемом платежей, общается с пользователями. Контейнер Offline, который проводит сам платеж. Описывающая OLTP база данных и бэкофис, который занят реестрами, согласованиями, проверками. На этой диаграмме видны практически все потоки, вся архитектура.

@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Dynamic.puml
title Динамическая диаграмма (3 уровень)
LAYOUT_WITH_LEGEND()

ContainerDb(oltp, "OLTP Database", "PostgreSQL Database", "Хранит в себе данные об активных транзакциях, активных лимитах, актуальный срез справочных данных для проведения онлайн транзакций")
ContainerDb(wh, "Warehouse Database", "PostgreSQL Database", "Хранит в себе все исторические данные, реестры, товары и т.д.,")

Person(user, "Пользователь")

Container_Boundary(bOnline, "Online") {
  Component(api, "Api Service", "Spring MVC Rest Controller", "Позволяет пользователям создавать платежи")
  Component(limits, "Limits Service", "Spring Bean", "Проверяет лимиты пользователей")
  Component(otp, "OTP Service", "Spring Bean", "Отправляет OTP пользователям")
  Component(processing, "Processing Service", "Spring Bean", "Создают транзакции в базе")
}

System_Ext(sms, "Sms Gateway")

System_Ext(paysys, "Платежная система, например Qiwi Wallet")
Container_Boundary(bOffline, "Offline") {
    Component(paydealer, "Проведение платежей", "Spring Bean", "Забирает данные о транзакциях из базы и отправляет их в платежную систему")
    Component(notification, "Сервис нотификации", "Spring Bean", "Отправляет пользователям информацию о финальности их транзакций")
}

System_Ext(backoffice, "BackOffice", Учет реестров и взаимоотношений с платежными системи")
Rel(user, api, "Создают платежи")
Rel(otp, sms, "Вставляет смс в очередь на отправку")
Rel(sms, user, "Отправляет пользователям sms")
Rel_D(limits, oltp, "Проверка актуальный лимитов")
Rel_D(oltp, wh, "Постоянная репликация")
Rel_R(paydealer, paysys, "Отправляет информацию о платежах")
Rel_U(notification, user, "Информация о статусе платежей")
Rel_U(paydealer, oltp, "Информация о новых платежах")
Rel_D(wh, backoffice, "Использует данные")
@enduml

Выводы

PlantUML это не только игрушка, позволяющая делать красивые диаграммы. Вы можете спокойно хранить свои диаграммы в CVS в каком-нибудь гите со спокойной совестью. Можете добавить ее в пайплайн, когда деплоите на прод. Если вы обновляете диаграмму у себя в документации, не нужно следить за тем, что документация устаревает. Если вы активно пользуетесь диаграммой и обновляете ее, новая версия тут же летит на сервер с документацией. 

И наконец, PlantUML поддерживается почти всеми популярными IDE: JetBrains, Eclipse, VsCode, много их. И есть огромное количество плагинов. 

Полезные ссылки

Для тех, кто любит формат видео — запись доклада.

Комментарии (18)


  1. ArsenAbakarov
    16.09.2021 13:55
    +2

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


  1. trak
    16.09.2021 15:43
    +1

    Я люблю PlantUML, единственное, что я как-то умудрился нарисовать такую диаграмму, которая после загрузки в конфлюэнс его крешила. Так я и не понял, чем я так ему не угодил :)


    1. fjfalcon Автор
      16.09.2021 16:31

      У движка конфлюенса иногда не хватает памяти для graphviz, чтобы собрать особо сложную диаграмму.


  1. js605451
    16.09.2021 16:13
    +4

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

    Эта ссылка должна быть в статье: https://www.planttext.com/

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

    В итоге использую https://www.diagrams.net/ - он позволяет сохранять диаграммы как XML файлы (ну и дальше их конечно же можно в гит).


    1. fjfalcon Автор
      16.09.2021 18:22

      Инструментов много, суть моего доклада и данной статьи показать, как работает инструмент plantuml. Посмотреть diagrams - посмотрим. А для него есть плагины для confluence и других вики?


      1. js605451
        16.09.2021 18:45
        +1

        У вас очень поверхностная статья и все примеры гладкие. Реальность выглядит например вот так:

        (плагины для конфлюенсов конечно есть)


        1. Justerest
          16.09.2021 20:09
          +2

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


          1. js605451
            16.09.2021 23:09

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


            1. inkelyad
              20.09.2021 11:14

              Оно не техническое. Оно концептуальное. Если получается то что на картинке — это означает, что диаграмма становится бессмысленной (Начинаем рисовать всякие таблицы и вообще отображать ту же информацию по другому). Можно то же самое разложить в yEd который значительно богаче в смысле инструментов раскладывания диаграммы.


              Результат скорее всего получится приблизительно такой же.


              1. js605451
                20.09.2021 16:50

                Сформулируете концептуальное ограничение? Типа: "показать все реализации интерфейса на одной диаграмме - бессмысленная затея, никто и никогда не должен этого хотеть".


                1. inkelyad
                  21.09.2021 09:45

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


                  С текстом, например, это делается параграфами и главами.


                  "показать все реализации интерфейса на одной диаграмме" — ну так по мне действительно бессмысленная. Просто написать текстом "All Known Implementing Classes: <дальше список>" или нарисовать табличку с ними же — рассказывает о том же и дополнительно позволяет всяким поисковым движкам искать то, что тебе надо.


        1. fjfalcon Автор
          16.09.2021 21:10

          А какой смысл этой диаграммы? К сожалению, это как раз пример очень перегруженной диаграммы, смысл которой не ясен совсем. Это тоже самое, что взять продовую базу с 10^5 таблиц и сгенерировать таблицу связей. Хорошие диаграммы - это sequence диаграммы, state диаграммы. Что отображено в статье. Плюс модель с4. Хорошее видео по с4 model: https://www.youtube.com/watch?v=x2-rSnhpw0g


          1. js605451
            16.09.2021 23:40
            +1

            А какой смысл этой диаграммы? К сожалению, это как раз пример очень перегруженной диаграммы, смысл которой не ясен совсем. Это тоже самое, что взять продовую базу с 10^5 таблиц и сгенерировать таблицу связей.

            Да ну. Развесистые диаграммы бывают. С ними всё в порядке. Они полезный инструмент для "творческих" задач - когда толком не знаешь где начало, где конец, но хочешь посмотреть, пообсуждать. В таких ситуациях удобны срезы всей системы целиком. Берёшь такую диаграмму, и погнал: зачёркиваешь, исправляешь, добавляешь, и т.д.

            И неразвесистые диаграммы тоже бывают. Они полезный инструмент для более конкретных вопросов, типа "а как у нас классы и студенты связаны"? А вот вам картинка из 3 прямоугольничков: классы-энроллменты-студенты. На всю базу нет смысла смотреть.

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

            Хорошие диаграммы - это sequence диаграммы, state диаграммы. Что отображено в статье.

            Sequence диаграммы хорошо идут, потому что там компоновка совершенно предсказуемая. Любой другой тип, где в общем случае дерево/граф - и всё, "3 прямоугольничка должно быть достаточно каждому". Я ж вроде это и написал в первом комментарии?


        1. tzlom
          17.09.2021 08:59
          +1

          Ну если необходимо показать наслоения классов то либо так либо круговая. И мне кажется в PlantUML добавить что-то в диаграмму будет проще нежели в графическом интерфейсе.

          У меня есть диаграммы размером где-то А1 если печатать, если бы я руками расставлял в них квадратики я бы с ума сошел.


          1. js605451
            17.09.2021 16:22

            Вот исходник моей демонстрационной диаграммы:

            @startuml
            
            title Classes - Class Diagram
            
            interface Service
            class ServiceA implements Service
            class ServiceB implements Service
            class ServiceC implements Service
            class ServiceD implements Service
            class ServiceE implements Service
            class ServiceF implements Service
            class ServiceG implements Service
            class ServiceH implements Service
            class ServiceI implements Service
            class ServiceJ implements Service
            class ServiceK implements Service
            class ServiceL implements Service
            class ServiceM implements Service
            
            @enduml

            Покажете как круглый лэйаут сделать?


  1. rpsv
    17.09.2021 07:56

    С наступлением пандемии, когда все ушли на удаленку, для описания задач и фичей мы использовали notepad, textedit, sublime - все, что попадало под руку, чтоб зафиксировать текст.

    А таск менеджер использовать, нет? Он вроде и создан для того что копить и структурировать задачи.

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

    А статья хорошая и инструмент полезный и нужный :)


  1. klirichek
    17.09.2021 20:55
    +1

    У них ещё фишка есть (про интеграцию)

    Попробуйте где-нибудь на гитлабе (не гитхабе, хотя там тоже может сделают), вводить plantuml в соответствующем контейнере

    digraph foo {
    label="Elevation deadlock"
    
    Th1 -> Shared1 [label=" take 1r"]
    Th2 -> Shared2 [label=" take 1r"]
    }

    ... и тут вдруг внезапно оказывается, что встроенный plantuml (или всё, что может под ним скрываться, в данном случает dot) - после рендеринга превращается в картинку!
    Работает (на гитлабе) везде - заметки, тикеты, комментарии к тикетам, вики.
    (на гитхабе пока, к сожалению, просто рендерится код как текст. Но наверняка очень скоро исправится!)


  1. ivanych
    27.09.2021 01:23

    Чем просматривать?

    Как переходить между уровнями C4?