Я дико люблю ковыряться в чужом коде. Это одна из моих любимых специализаций. То есть я просто беру чужой код, анализирую его, читаю. Как я читал его раньше: я переводил код в русский язык. Описывал, что происходит по флоу кода, и пытался понять, что там происходит. Эти записи я в дальнейшем использовал как для написания статей в Confluence, так и для общего понимания происходящего.
С одной стороны, решение работающее. С другой, буквально через неделю-две я уже начинал сомневаться, достаточно точно ли я «перевел» с кода на русский язык? И тогда вспомнил про UML-диаграммы. И вместо того, чтобы записывать текст, стал визуализировать его и исписал неимоверное количество тетрадей.
Но в какой-то момент подумал, что хорошо бы перевести все это в электронный вид, чтобы какой-то инкремент оставался. Не фоткать же, например, для документации, свою тетрадь с каракулями. Так я нашел инструмент PlantUML — opensource-решение, которое использует графическую библиотеку graphviz, превращающее код в наглядные схемы.
Давайте вспомним, что такое Unified Modeling Language. Чаще всего в университете UML используется для описания диаграммы классов.
![](https://habrastorage.org/getpro/habr/upload_files/c94/a9e/385/c94a9e385b18177920027daac6684784.png)
У человека есть паспорт, у дома есть стены. У класса «дерево» есть птицы, дерево умеет сбрасывать листья. 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. Получим красивую схему, которую будет не стыдно вставить в рабочую документацию.
![](https://habrastorage.org/getpro/habr/upload_files/e04/697/906/e04697906dcd308185c6b4d552ced9cd.png)
Для чего я использую PlantUML в работе
Во-первых, это анализ чужого кода. Я разработчик процессинга Qiwi Кошелька, и у нас есть код, который занимается OAUTH2 авторизацией. Я читал его уже раз восемь, но через пару недель все равно забываю, как он работает. При том, что код лаконичный, динамичный, он позволяет делать, что угодно. Но каждый раз, когда я открываю классы, и вижу OAuthCore класс, думаю: “е-мое”. Когда я нарисовал схему, мне и моим коллегам стало гораздо проще ковырять этот код.
Во-вторых, для объяснения, как что-то работает. С наступлением пандемии, когда все ушли на удаленку, для описания задач и фичей мы использовали notepad, textedit, sublime - все, что попадало под руку, чтоб зафиксировать текст. Все это предсказуемо терялось, договоренности забывались. Преодолевать споры не помогали и письма в рабочую почту. Тогда я предложил своей команде использовать PlantUML для описания фичей, а именно стандартные Sequence и State диаграммы, которые позволяют сразу привести решение в порядок.
В-третьих, никто не отменял разработку архитектурных и прочих фичей. Когда мы переделываем архитектуру, разбираемся, как все устроено, компонентные диаграммы очень сильно помогают. И, наконец, документация. Куда без нее. В PlantUML мы спокойно делаем инкремент, который можно вставить в любую систему документирования.
Давайте посмотрим, как работает PlantUML на примере из продуктовой разработки.
Кейсы
Допустим, мы создаем маркетплейс — продукт QMarket.
Каждый пользователь может завести деньги на счет в системе
Каждый пользователь может выставлять товар в системе
Система позволяет продавать, используя арбитраж
Пользователи свободно могут выводить средства на карту
В него можно заводить и выводить деньги, есть какой-то счет. В общем, стандартный пример, ничего нового. И есть команда, которая занимается процессингом платежей. Наша задача: подготовить флоу по отправке запросов на выплату.
Вместо того, чтобы описывать задачу текстом, нарисуем диаграмму.
![](https://habrastorage.org/getpro/habr/upload_files/fd4/240/ad5/fd4240ad5a563eda4bae81128c27980e.jpeg)
Участник — 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 диаграмма.
![](https://habrastorage.org/getpro/habr/upload_files/f49/f87/ca3/f49f87ca36d964ad03c505f856574490.jpeg)
Платеж уже создан, и есть два кейса: лимиты превышены или лимиты пройдены. Если лимиты превышены, мы идем в самый конец, в точку, где платеж не удалось выполнить. Если лимиты пройдены, то проверяем смс-подтверждение, получаем от пользователя 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
}
}
}
Посмотрим на этот код в виде диаграммы.
![](https://habrastorage.org/getpro/habr/upload_files/581/e4f/daa/581e4fdaab67d212589c232dc6b12000.jpeg)
Офлайн-процессинг забирает из базы транзакции, ставит статус “отправлено в платежную систему” и только потом действительно отправляет. Если вдруг на втором шаге что-то случится с сервисом, который все это отправляет, транзакция зависнет. Мы будем считать, что транзакция отправлена в платежную систему, но на самом деле это не так. Ошибка, баг. Сложно было нарисовать схему? Нет, всего семь строчек.
@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 диаграммы, которые я только что показал.
![Пример диаграммы системы Пример диаграммы системы](https://habrastorage.org/getpro/habr/upload_files/4d9/24c/c53/4d924cc533aace94441b1c511ab2b531.jpeg)
На что здесь обратить внимание? Пользователи, основные участники, основаны на абстракциях. Кроме названия, самого 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 и другие. После определения всех участников диаграммы мы продолжаем рассказывать про связи.
Я решил заморочиться и нарисовать схему процессинга, который мы задумали в кейсе с маркетплейсом. Получилась большая динамическая диаграмма.
![](https://habrastorage.org/getpro/habr/upload_files/7ad/4d8/599/7ad4d8599e8f3b67bb9cd9afd613ddf7.png)
В ней есть контейнер 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, много их. И есть огромное количество плагинов.
Полезные ссылки
https://github.com/fjfalcon/qsp6 - исходники статьи
https://plantuml.com/ - сайт с отличной документацией по PlantUML
https://real-world-plantuml.com/ - сайт с примерами использования
https://crashedmind.github.io/PlantUMLHitchhikersGuide/ - еще один хороший гайд про “как начать пользоваться”
https://github.com/plantuml-stdlib/C4-PlantUML - использование plantuml и C4 model
Для тех, кто любит формат видео — запись доклада.