
Привет Хабр! Меня зовут Татьяна Ошуркова, я системный аналитик, разработчик и автор телеграм-канала IT Talks. Несмотря на то, что UML-диаграммы являются популярным и востребованным инструментом, не все системные аналитики используют его в своей работе. Одной из причин может быть непонимание пользы для требований и проработки задачи.
Скажу честно, что такое было и со мной, когда опыта у меня было немного. Сложностей в работе хватало и без дополнительных инструментов, которые требовали время на погружение. В этой статье я на практическом примере покажу, как проработать процесс с помощью моделирования на UML. Подробно посмотрим, как сделать это быстро, понятно, а также выделим основные преимущества решения.
А ещё в моем канале ты можешь скачать бесплатный гайд с шаблонами диаграмм на PlantUML.
Постановка задачи
Рассмотрим процесс интеграции нескольких систем. Цель процесса – собрать необходимые данные для нашей системы из внешних систем. Предположим, нам необходимо собрать данные типа А и Б. Процесс в нашей системе стартует после получения данных о событии из топика внешней системы. Сообщение об этом получит пользователь, после чего запустит получение данных в нашей системе.
В процессе будут участвовать следующие объекты:
Пользователь, который работает с целевым процессом.
Внешняя система, отправляющая события в топик Kafka. Например, создание нового процесса. Это будет нашей стартовой точкой.
Наша система. На практике у неё может быть любое назначение.
Внешний сервис для обработки данных типа А.
Внешний сервис для обработки данных типа В.
База данных нашей системы, где мы будем сохранять полученные данные.
Процесс начинается с получения нашей системы сообщения из топика. Сообщение обрабатывается, после чего пользователь получает уведомление. После этого он вручную запускает основной процесс. Система запрашивает данные объекта типа A у внешнего сервиса A и сохраняет их во внутреннее хранилище. Далее осуществляется запрос списка дочерних объектов, таких как подчинённые элементы, вложенные сущности или связанные документы, которые также сохраняются. После этого каждый дочерний объект обрабатывается индивидуально: для каждого выполняется отдельный запрос, и результат сохраняется. Затем сервис делает запрос на получение дополнительных данных по объекту A. Если этих данных нет, выполняется альтернативный запрос во внешний сервис B для получения данных типа B. В финале все собранные данные сохраняются, и пользователь получает уведомление о завершении процесса. Всё взаимодействие между компонентами визуализируется с помощью диаграммы последовательности UML, позволяющей чётко отследить порядок действий и участников.
Проблематика
Как сразу видно из постановки задачи, описание процесса сплошным текстом тяжело воспринимается. Особенно, если обогатить описание техническими данными.
Сложность данной задачи заключается в том, что данные будут получены через отправку множества запросов. А каждый следующий запрос будет использовать данные предыдущего. Нужно проработать зависимости запросов и систем друг от друга. Также важна последовательность выполняемых шагов. Визуально это будет сделать легче и понятнее всего.
Для таких задачи идеально подходит использование диаграмм в работе. Так как мы описываем не бизнес-процесс, то в нашем случае диаграмма BPMN не подойдет. Нужно использовать UML-диаграммы. Выбирая подходящий тип диаграммы нужно учесть, что мы работаем с последовательностью действий нескольких систем. Также нам важен результат каждого шага и их зависимости. Поэтому для формализации и анализа взаимодействия объектов используем диаграмму последовательности (Sequence diagram).
Решение задачи
Рассмотрим пошаговую проработку процесса и построение диаграммы. Нам необходимо смоделировать процесс получения и обработки данных типа A и B из внешних сервисов в нашей системе.
Шаг 1. Определение состава участников
На этом этапе мы определяем всех участников процесса. Это помогает визуально отразить, кто и с кем взаимодействует. Каждый участник диаграммы будет либо инициировать действие, либо принимать участие в обмене информацией:
actor Пользователь as user
participant "Event Stream" as eventStream
participant "Processing Service" as procServ
participant "Service A" as serviceA
participant "Service B" as serviceB
participant "Storage" as storage

Шаг 2. Запуск процесса событием из внешней системы
Внешняя система отправляет сообщение о создании процесса в топик Kafka. Наша система получает сообщение и уведомляет пользователя о начале процесса, это первый шаг на диаграмме:
eventStream -> procServ : Событие создания процесса
activate procServ
procServ --> user : Уведомление о начале обработки
deactivate procServ

Здесь важно показать, что процесс не запускается автоматически, а требует участия пользователя после уведомления.
Шаг 3. Запуск процесса пользователем
Пользователь инициирует процесс получения данных вручную. Это может быть вызов в интерфейсе или нажатие кнопки:
user -> procServ : Запуск процесса
activate procServ

Шаг 4. Получение данных типа A
Система запрашивает объект типа A по его идентификатору. Полученные данные сохраняются в базе данных. Важно показать успешное получение и сохранение результата:
procServ -> serviceA : GET /entityA/{id}
activate serviceA
serviceA --> procServ : Данные объекта
deactivate serviceA
procServ -> storage : Сохранение данных
activate storage
storage --> procServ : OK
deactivate storage

Шаг 5. Получение связанных объектов
После получения основного объекта типа A, необходимо получить связанные с ним дочерние объекты. В зависимости от логики предметной области это могут быть, например, вложенные сущности, элементы иерархии или связанные компоненты, такие как документы, подэлементы или подчинённые записи. Эти дочерние объекты представляют собой логически связанные с основным объектом данные, без которых дальнейшая обработка может быть невозможна или неполной. После получения их списка, каждый из них также будет обработан отдельно и сохранён в хранилище, поскольку они участвуют в общем процессе анализа и формирования бизнес-результатов:
procServ -> serviceA : GET /entityA-group/{id}
activate serviceA
serviceA --> procServ : Список дочерних объекта
deactivate serviceA
procServ -> storage : Сохранение дочерних объектов
activate storage
storage --> procServ : OK
deactivate storage

Шаг 6. Обработка дочерних объектов в цикле
Каждый дочерний объект требует отдельной загрузки и сохранения, поскольку представляет собой уникальную единицу информации, связанную с основным объектом. Для отражения повторяющегося характера таких операций используется конструкция loop
, которая показывает, что действия будут выполняться многократно – по одному разу для каждого дочернего объекта. Такой подход помогает отразить как саму итеративную природу обработки, так и масштабируемость сценария:
loop Получение данных дочерних объектов
procServ -> serviceA : GET /entityA/{id}
activate serviceA
serviceA --> procServ : Данные объекта
deactivate serviceA
procServ -> storage : Сохранение данных по объекту
activate storage
storage --> procServ : OK
deactivate storage
end

Шаг 7. Получение дополнительных данных объекта A
После загрузки основной информации может потребоваться получить дополнительные данные. Это отдельный вызов, не связанный напрямую с предыдущими:
procServ -> serviceA : GET /entityA/{id}/data
activate serviceA
serviceA --> procServ : Доп. данные объекта А
deactivate serviceA

Шаг 8. Обработка альтернативных данных (условие opt)
Если дополнительные данные отсутствуют, выполняется альтернативный запрос в другой сервис. Это показано с использованием конструкции opt
, которая обозначает условное выполнение:
opt Доп. данные отсутствуют
procServ -> serviceB : GET /entityB/{id}/data
activate serviceB
serviceB --> procServ : Данные Б
deactivate serviceB
end

Шаг 9. Финальное сохранение данных и завершение процесса
После завершения всех запросов сохраняются финальные данные, и пользователь уведомляется о завершении процесса:
procServ -> storage : Сохранение данных Б
activate storage
storage --> procServ : OK
deactivate storage
procServ --> user : Процесс завершён
deactivate procServ

Преимущества и недостатки
Я часто рассказываю о том, что диаграммы полезны не только в моделировании требований или визуализации процессов. Они помогают мне погрузится в процесс, понять его и уложить «по полочкам» всю информацию о нём.
Поэтому, на мой взгляд, диаграммы полезны не только для тех, кто будет работать с нашими требованиями, но и для нас самих. Они помогают структурировать информацию, выявить возможные пробелы или противоречия в логике, убедиться, что все взаимодействия продуманы и зафиксированы. Диаграмма становится не только средством коммуникации, но и инструментом анализа и самопроверки.
При всех преимуществах диаграммы последовательности имеют и определённые недостатки. Один из ключевых – необходимость поддерживать диаграммы в актуальном состоянии: при изменении логики процесса или требований они могут быстро устаревать. Ещё один момент – порог входа: для понимания и особенно построения таких диаграмм требуется время на погружение в нотацию и в сам процесс. Однако это оправдывает себя: в результате мы получаем наглядную и структурированную картину взаимодействий, которая упрощает коммуникацию внутри команды и снижает риски недопонимания на этапе разработки и тестирования.
Подведем итоги
Думаю, что нельзя не согласиться с тем, что описанная в статье задача, решалась бы дольше и сложнее без использования диаграммы в работе. Для её построения были использованы несколько базовых элементов нотации. В построении диаграмма достаточно простая, хотя в конечном итоге она выглядит достаточно сложной и содержит большое количество данных.
По моему опыту такая проработка – идеальный вариант для процессов с несколькими участниками и обменом информации, где важны технические детали, зависимости и последовательность шагов.
В завершение делюсь полным исходным кодом диаграммы на PlantUML:
Исходный код на PlantUML
@startuml
autonumber
actor Пользователь as user
participant "Event Stream" as eventStream
participant "Processing Service" as procServ
participant "Service A" as serviceA
participant "Service B" as serviceB
participant "Storage" as storage
eventStream -> procServ : Событие создания процесса
activate procServ
procServ --> user : Уведомление о начале обработки
deactivate procServ
user -> procServ : Запуск процесса
activate procServ
procServ -> serviceA : GET /entityA/{id}
activate serviceA
serviceA --> procServ : Данные объекта
deactivate serviceA
procServ -> storage : Сохранение данных
activate storage
storage --> procServ : OK
deactivate storage
procServ -> serviceA : GET /entityA-group/{id}
activate serviceA
serviceA --> procServ : Список дочерних объекта
deactivate serviceA
procServ -> storage : Сохранение дочерних объектов
activate storage
storage --> procServ : OK
deactivate storage
loop Получение данных дочерних объектов
procServ -> serviceA : GET /entityA/{id}
activate serviceA
serviceA --> procServ : Данные объекта
deactivate serviceA
procServ -> storage : Сохранение данных по объекту
activate storage
storage --> procServ : OK
deactivate storage
end
procServ -> serviceA : GET /entityA/{id}/data
activate serviceA
serviceA --> procServ : Доп. данные объекта А
deactivate serviceA
opt Доп. данные отсутствуют
procServ -> serviceB : GET /entityB/{id}/data
activate serviceB
serviceB --> procServ : Данные Б
deactivate serviceB
end
procServ -> storage : Сохранение данных Б
activate storage
storage --> procServ : OK
deactivate storage
procServ --> user : Процесс завершён
deactivate procServ
@enduml