Следить за обновлениями блога можно в моём канале: Эргономичный код

Это второй пост в серии, посвящённый диаграмме эффектов:

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

  2. "Пример построения, проект True Story Project (TSP)" - процесс построения диаграммы эффектов реального проекта.

  3. "Декомпозиция системы на модули на базе диаграммы эффектов" - рациональный подход к разбиению системы на модули с помощью диаграммы эффектов и его применение для декомпозиции проекта TSP.

  4. "Перевод диаграммы в код" - процесс трансляции диаграммы в исходный код на примере проекта TSP.

Введение

Ведущие разработчики (ака техлиды, тимлиды, архитекторы) встречаются с целым рядом нетривиальных вопросов:

  1. Как оценить трудоёмкость проекта?

  2. Как обеспечить низкую сцепленность (читай: низкую стоимость) системы?

  3. В каком порядке реализовывать части системы и как эффективно распараллелить работу?

  4. И ключевой вопрос - а что вообще надо сделать-то?

Я для поиска ответов на эти вопросы использую диаграмму эффектов. Чтобы научить этому и свою команду, я поэтапно описал процесс создания диаграммы эффектов своего последнего коммерческого проекта.

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

Диаграмма эффектов системы актуализации данных в Яндекс.Картах и 2Гис (True Story Project, TSP)

Я только что сделал небольшой проект по автоматизации обновления информации о компании в Яндекс.Картах и 2Гис. Это идеальный проект для иллюстрации диаграммы эффектов - он небольшой, но включает все основные типы событий и ресурсов (картинка кликабельна):

Все новые проекты я начинаю с диаграммы эффектов и этому есть ряд причин.

Самая главная: построив диаграмму эффектов, я получаю целостную картинку того, что необходимо сделать.

Затем, на этапе оценки, формула (<кол-во операций>+<кол-во ресурсов>) * 8 часов даёт хорошее первое приближение трудоёмкости работы, если все операции и ресурсы типовые. По этой формуле ожидаемая трудоёмкость реализации проекта TSP составляет 14 * 8 = 112 часов. А фактически весь проект я сделал за 130 часов, включая построение диаграммы эффектов и всевозможные административные издержки.

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

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

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

Процесс построения диаграммы

Этот проект я сделал по "Time&Material", поэтому на самом деле в рамках реализации сделал не полную диаграмму, приведённую выше, а краткую с событиями - её построение я и опишу.

Итак, поехали. На старте у меня было ТЗ, которое можно ужать до следующих пунктов:

  1. У заказчика есть проблема: ему необходимо держать информацию о его организациях в ряде геосервисов в актуальном состоянии.

  2. На первом этапе необходимо актуализировать информацию в Яндекс.Картах и 2Гис.

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

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

  5. Яндекс требует, чтобы фид всегда был доступен по заданному URL, поэтому необходимо обеспечить постоянное хранение последнего сгенерированного фида.

  6. Интеграция с 2Гисом выполняется посредством отправки фида на Email, но уже в формате xlsx.

  7. Список организаций необходимо получать из специального сервиса заказчика по REST API.

  8. Ещё часть данных хранится во внутренней СУБД заказчика и извлекается с помощью JDBC и SQL-запроса, предоставленного заказчиком.

  9. Наконец, фид может содержать ссылки на фотографии организаций, и управление этими фотографиями должен обеспечить разрабатываемый сервис. Доступ к этой функциональности должен быть обеспечен посредством REST API. Конкретное хранилище изображений можно выбрать на своё усмотрение.

Работу можно начать сразу с построения диаграммы эффектов, но я обычно в первом проходе составляю просто списки событий, операций и ресурсов, т.к. по мере вычитки ТЗ их состав наверняка будет меняться и уточняться. Кроме того, при первой вычитке ТЗ я обычно строю ER-диаграмму, но в данном случае модель данных примитивная я не буду усложнять пример её построением.

Поэтому давайте пройдёмся по "ТЗ" и сделаем на его основе три артефакта: список операций системы, список событий системы и список ресурсов.

Шаг №1: построить списки событий, операций и ресурсов

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

Пройдёмся по пунктам ТЗ.

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

Из этого пункта мы можем сделать предположение, что у нас будет ресурс "Коллекция организаций", но пока не понятно, как он будет реализован . Добавляем его в соответствующий список со знаком "?".

На первом этапе необходимо актуализировать информацию в Яндекс.Картах и 2Гис.

Пункт №2 говорит о том, что нам потребуются ещё 2 ресурса - "Интеграция с Яндекс.Картами" и "Интеграция с 2Гис" - также добавляем в список с "?".

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

Этот пункт даёт нам событие "Scheduler: Истёк срок действия фида" и операцию "Обновить фид" - добавляем в списки событий и операций. Уже сейчас понятно, что эта операция будет связана со всеми известными на данный момент ресурсами - можно это как-то отметить в списке, но я на первом проходе не обозначаю связи, чтобы держать списки максимально простыми.

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

Пункт №4 проясняет интеграцию с Яндексом. На самом деле у нас pull-модель (Яндекс "вытягивает" фид из нашей системы), а не push-модель (когда мы "толкаем" фид в Яндекс). Это даёт нам новое событие и операцию - "HTTP: Запрос фида" и операцию "Выдать фид Яндекса".

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

Яндекс требует, чтобы фид всегда был доступен по заданному URL, поэтому необходимо обеспечить постоянное хранение последнего сгенерированного фида.

Пункт №5 дальше уточняет этот ресурс - это должен быть какой-то постоянный кэш, добавляем соответствующую пометку в список.

Интеграция с 2Гисом выполняется посредством отправки фида на Email, но уже в формате xlsx.

Этот пункт проясняет способ реализации интеграции с 2Гис - Email, уточняем его в списке.

Список организаций необходимо получать из специального сервиса заказчика по REST API.

Пункт №7 уточняет способ реализации ресурса "Коллекция организаций" - REST, уточняем его в списке.

Ещё часть данных хранится во внутренней СУБД заказчика и извлекается с помощью JDBC и SQL-запроса, предоставленного заказчиком.

Пункт №8 определяет ещё один ресурс операции "Обновить фид" - "JDBC: Дополнительная информация", добавляем его в список.

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

Пункт №9 определяет новый ресурс "Изображения" и набор операций "Загрузить изображение", "Скачать изображение", "Выдать список изображений организации", "Удалить изображение", с набором соответствующих событий об обращениях к HTTP-эндпоинтам.

В итоге у нас получились следующие списки.

События:

  1. Scheduler: Истёк срок действия фида;

  2. HTTP: Запрос фида;

  3. HTTP: Запрос загрузки нового изображения;

  4. HTTP: Запрос изображения;

  5. HTTP: Запрос списка изображений организации;

  6. HTTP: Запрос удаления изображения.

Операции:

  1. Обновить фид;

  2. Выдать фид Яндекса;

  3. Загрузить изображение;

  4. Скачать изображение;

  5. Выдать список изображений организации;

  6. Удалить изображение.

Ресурсы:

  1. REST: Коллекция организаций;

  2. Постоянный Кэш?: Фид Яндекса;

  3. Email Server: Интеграция с 2Гис;

  4. JDBC: дополнительная информация;

  5. ???: Изображения.

Теперь построим диаграмму эффектов, просто перенося в неё элементы списков, попутно отмечая связи между ними.

Шаг №2: нарисовать остальную сову (построить диаграмму эффектов)

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

Например, если начать с первого события "Истёк срок действия фида", то мы раскрутим сразу половину диаграммы - само событие "Истёк срок действия фида", операцию "Обновить фид", ресурсы "Коллекция организаций", "Дополнительная информация", "Изображения", "Фид Яндекса" и "Интеграция с 2Гис" Добавляем всё это на диаграмму, связываем операции с ресурсами соответствующими эффектами и получаем примерно такую картину:

После взгляда на эту диаграмму у меня загорается "алярма!" - две красные стрелки из одной операции часто свидетельствуют о нарушении одного из принципов проектирования:

  1. низкой сцепленности, высокой связности;

  2. единственности ответственности;

  3. открытости/закрытости.

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

Самым простым и универсальным способом расцепить эффекты записи является шина событий. В нашем случае она вполне подойдёт. Для того чтобы провести этот "рефакторинг" нам надо добавить новый ресурс "Тема (Topic) 'Сгенерирован новый фид'" и соответствующее событие "Оповещение о генерации нового фида", которое будет обрабатываться новыми операциями "Обновить фид Яндекса" и "Отправить фид в 2Гис". Добавив всё это на диаграмму (про списки можно уже забыть), получаем новую версию:

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

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

Ещё я подумаю, что мне надо будет убедиться в том, что внешние ресурсы предоставляют мне нужное API. В частности, при выборе способа реализации ресурса "Изображения", который у меня пока под вопросом, мне надо будет убедиться, что выбранный способ обеспечит возможность хранения привязки файлов изображений к организациям. Но я это пока просто помечу в заметках по проекту и продолжу строить диаграмму эффектов.

На этом ветка обработки события "Истёк срок действия фида" у нас заканчивается и мы можем переходить к следующему событию - "Запрос фида". Для этого события уже всё готово - осталось только привязать его к ресурсу "Фид Яндекса" через операцию "Выдать фид Яндекса".

Далее мы аналогичным образом добавляем на диаграмму события и операции, связанные с изображениями.

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

Но при ближайшем рассмотрении выясняется, что с фотографиями есть нюанс - помимо операций по ключу есть и поиск по организации. Теоретически это можно реализовать посредством bucket-ов или "папок" S3, но на мой вкус это решение уже начинает дурно пахнуть. А чуть позже, когда мы внимательнее изучим формат фида Яндекса, мы увидим, что у фотографий есть ещё и метаинформация в виде типа и тэгов - хранить это в S3 будет уже совсем плохой идеей. Значит нам нужна более продвинутая СУБД. У меня "продвинутой СУБД по умолчанию" является PostgreSQL.

PostgreSQL отлично справится с хранением привязки и метаинформации изображений, но он не предназначен для хранения сотен гигабайт самих изображений. Поэтому абстрактный ресурс "Изображения" будет состоять из двух технических частей - "Файлы" и "Метаинформация". Это нам создаст определённые проблемы с согласованностью данных (существование файла без метаинформации или наоборот). Однако, это будет не критично, если упорядочить операции добавления и удаления правильным образом - добавление файла, добавление меты, удаление меты, удаление файла. В этом случае проблема будет заключаться в том, что в случае ошибок, в S3 будут оставаться мусорные файлы, но если это вдруг действительно станет проблемой - можно будет достаточно безболезненно реализовать сборку этого мусора.

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

В итоге получаем финальную версию диаграммы эффектов проекта True Story в краткой нотации с событиями:

Заключение

Построение диаграммы эффектов уже дало нам много полезных штук:

  1. Хорошее представление о том, что надо сделать - какие операции есть у системы и что они должны делать.

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

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

  4. Интуитивно-понятную иллюстрацию для декомпозиции системы на модули - вы же тоже видите на диаграмме модули изображений, фида, интеграции с Яндексом и 2Гисом?

Но вернёмся к нашим изначальным вопросам:

  1. Как оценить трудоёмкость задачи?

  2. Как обеспечить низкую сцепленность системы?

  3. В каком порядке реализовывать части системы и как эффективно распараллелить работу?

  4. И ключевой вопрос - а что вообще надо сделать-то?

На последний вопрос мы получили исчерпывающий ответ.

Для ответа на первый вопрос у нас появились все входные данные и осталась только механическая работа по оценке простых и понятных блоков.

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

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


  1. komissar1976
    15.06.2022 10:47
    +1

    Автору респект! Первая картинка мелкая, нечитабельна на 17" :(


    1. jdev Автор
      15.06.2022 10:53

      К сожалению сама картинка здоровая получилась, а хабр, кажется, не может нормально увеличивать в превью. Могу предложить открыть по прямой ссылке - у меня в ФФ она нормально масштабируется