Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Следить за обновлениями блога можно в моём канале: Эргономичный код
Это второй пост в серии, посвящённый диаграмме эффектов:
"Спецификация" - назначение диаграммы, основные концептуальные элементы и их визуальное представление.
"Пример построения, проект True Story Project (TSP)" - процесс построения диаграммы эффектов реального проекта.
"Декомпозиция системы на модули на базе диаграммы эффектов" - рациональный подход к разбиению системы на модули с помощью диаграммы эффектов и его применение для декомпозиции проекта TSP.
"Перевод диаграммы в код" - процесс трансляции диаграммы в исходный код на примере проекта TSP.
Введение
Ведущие разработчики (ака техлиды, тимлиды, архитекторы) встречаются с целым рядом нетривиальных вопросов:
Как оценить трудоёмкость проекта?
Как обеспечить низкую сцепленность (читай: низкую стоимость) системы?
В каком порядке реализовывать части системы и как эффективно распараллелить работу?
И ключевой вопрос - а что вообще надо сделать-то?
Я для поиска ответов на эти вопросы использую диаграмму эффектов. Чтобы научить этому и свою команду, я поэтапно описал процесс создания диаграммы эффектов своего последнего коммерческого проекта.
Думаю, эта методика будет полезна не только моей команде, но и всем разработчикам, которые хотят научиться уверенно находить ответы на эти вопросы.
Диаграмма эффектов системы актуализации данных в Яндекс.Картах и 2Гис (True Story Project, TSP)
Я только что сделал небольшой проект по автоматизации обновления информации о компании в Яндекс.Картах и 2Гис. Это идеальный проект для иллюстрации диаграммы эффектов - он небольшой, но включает все основные типы событий и ресурсов (картинка кликабельна):
Все новые проекты я начинаю с диаграммы эффектов и этому есть ряд причин.
Самая главная: построив диаграмму эффектов, я получаю целостную картинку того, что необходимо сделать.
Затем, на этапе оценки, формула (<кол-во операций>+<кол-во ресурсов>) * 8 часов
даёт хорошее первое приближение трудоёмкости работы, если все операции и ресурсы типовые. По этой формуле ожидаемая трудоёмкость реализации проекта TSP составляет 14 * 8 = 112
часов. А фактически весь проект я сделал за 130 часов, включая построение диаграммы эффектов и всевозможные административные издержки.
Глядя на диаграмму эффектов, я могу убедиться, что ничего забыл и понимаю, как реализовать каждую функцию системы. Это даёт мне высокую степень уверенности в точности оценки.
Далее, на этапе проектирования, я использую диаграмму эффектов для разбиения системы на модули с высокой связанностью и низкой сцепленностью.
Наконец, на этапе планирования работ полученные модули, а также явные и, что важнее, неявные связи между ними я использую для определения порядка выполнения и возможности распараллеливания работ.
Процесс построения диаграммы
Этот проект я сделал по "Time&Material", поэтому на самом деле в рамках реализации сделал не полную диаграмму, приведённую выше, а краткую с событиями - её построение я и опишу.
Итак, поехали. На старте у меня было ТЗ, которое можно ужать до следующих пунктов:
У заказчика есть проблема: ему необходимо держать информацию о его организациях в ряде геосервисов в актуальном состоянии.
На первом этапе необходимо актуализировать информацию в Яндекс.Картах и 2Гис.
Организаций у заказчика существенно больше тысячи и построение фида (жаргонное название списка организаций заказчика) требует существенного времени и ресурсов. Поэтому фид необходимо обновлять по расписанию.
Интеграция с Яндекс Картами заключается в том, что робот Яндекса приходит на специально выделенный URL и забирает оттуда фид в XML-формате.
Яндекс требует, чтобы фид всегда был доступен по заданному URL, поэтому необходимо обеспечить постоянное хранение последнего сгенерированного фида.
Интеграция с 2Гисом выполняется посредством отправки фида на Email, но уже в формате xlsx.
Список организаций необходимо получать из специального сервиса заказчика по REST API.
Ещё часть данных хранится во внутренней СУБД заказчика и извлекается с помощью JDBC и SQL-запроса, предоставленного заказчиком.
Наконец, фид может содержать ссылки на фотографии организаций, и управление этими фотографиями должен обеспечить разрабатываемый сервис. Доступ к этой функциональности должен быть обеспечен посредством 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-эндпоинтам. |
В итоге у нас получились следующие списки.
События:
Scheduler: Истёк срок действия фида;
HTTP: Запрос фида;
HTTP: Запрос загрузки нового изображения;
HTTP: Запрос изображения;
HTTP: Запрос списка изображений организации;
HTTP: Запрос удаления изображения.
Операции:
Обновить фид;
Выдать фид Яндекса;
Загрузить изображение;
Скачать изображение;
Выдать список изображений организации;
Удалить изображение.
Ресурсы:
REST: Коллекция организаций;
Постоянный Кэш?: Фид Яндекса;
Email Server: Интеграция с 2Гис;
JDBC: дополнительная информация;
???: Изображения.
Теперь построим диаграмму эффектов, просто перенося в неё элементы списков, попутно отмечая связи между ними.
Шаг №2: нарисовать остальную сову (построить диаграмму эффектов)
Как именно переносить элементы из списков на диаграмму - сверху вниз, снизу вверх или в случайном порядке - не так важно. Я предпочитаю идти по событиям, раскрывая целиком все эффекты этого события.
Например, если начать с первого события "Истёк срок действия фида", то мы раскрутим сразу половину диаграммы - само событие "Истёк срок действия фида", операцию "Обновить фид", ресурсы "Коллекция организаций", "Дополнительная информация", "Изображения", "Фид Яндекса" и "Интеграция с 2Гис" Добавляем всё это на диаграмму, связываем операции с ресурсами соответствующими эффектами и получаем примерно такую картину:
После взгляда на эту диаграмму у меня загорается "алярма!" - две красные стрелки из одной операции часто свидетельствуют о нарушении одного из принципов проектирования:
низкой сцепленности, высокой связности;
единственности ответственности;
открытости/закрытости.
Это не всегда так, но в данном случае текущая версия диаграммы точно нарушает третий из них - добавление нового геосервиса потребует модификации существующего кода. А у нас в бэклоге, по секрету, болтается ещё потенциальная интеграция с Гуглом. Про сцепленность и единственность ответственности тоже можно порассуждать, но не буду, чтобы не размывать фокус поста.
Самым простым и универсальным способом расцепить эффекты записи является шина событий. В нашем случае она вполне подойдёт. Для того чтобы провести этот "рефакторинг" нам надо добавить новый ресурс "Тема (Topic) 'Сгенерирован новый фид'" и соответствующее событие "Оповещение о генерации нового фида", которое будет обрабатываться новыми операциями "Обновить фид Яндекса" и "Отправить фид в 2Гис". Добавив всё это на диаграмму (про списки можно уже забыть), получаем новую версию:
Теперь обновление фида открыто для расширения новыми интеграциями без изменения существующего кода.
Затем я обращаю внимание на то, что операция "Обновить фид" будет достаточно сложной, так как требует много ресурсов. Поэтому я задумываюсь о том, как она будет реализована - "я пробегусь по списку организаций, для каждой организации подтяну дополнительную информацию и изображения, данные свяжу через такие-то поля - все необходимые ресурсы есть, верхнеуровнево всё понятно", подумаю я.
Ещё я подумаю, что мне надо будет убедиться в том, что внешние ресурсы предоставляют мне нужное API. В частности, при выборе способа реализации ресурса "Изображения", который у меня пока под вопросом, мне надо будет убедиться, что выбранный способ обеспечит возможность хранения привязки файлов изображений к организациям. Но я это пока просто помечу в заметках по проекту и продолжу строить диаграмму эффектов.
На этом ветка обработки события "Истёк срок действия фида" у нас заканчивается и мы можем переходить к следующему событию - "Запрос фида". Для этого события уже всё готово - осталось только привязать его к ресурсу "Фид Яндекса" через операцию "Выдать фид Яндекса".
Далее мы аналогичным образом добавляем на диаграмму события и операции, связанные с изображениями.
Теперь по реализации осталось два вопроса: как реализовать ресурсы "фид Яндекса" и "Изображения"? Сами фотографии явно лучше хранить в хранилище BLOB-ов вроде Amazon S3. Там же можно хранить и фид Яндекса - у этого ресурса тривиальное API сохранения и получения файла по ключу, а размер файла исчисляется мегабайтами.
Но при ближайшем рассмотрении выясняется, что с фотографиями есть нюанс - помимо операций по ключу есть и поиск по организации. Теоретически это можно реализовать посредством bucket-ов или "папок" S3, но на мой вкус это решение уже начинает дурно пахнуть. А чуть позже, когда мы внимательнее изучим формат фида Яндекса, мы увидим, что у фотографий есть ещё и метаинформация в виде типа и тэгов - хранить это в S3 будет уже совсем плохой идеей. Значит нам нужна более продвинутая СУБД. У меня "продвинутой СУБД по умолчанию" является PostgreSQL.
PostgreSQL отлично справится с хранением привязки и метаинформации изображений, но он не предназначен для хранения сотен гигабайт самих изображений. Поэтому абстрактный ресурс "Изображения" будет состоять из двух технических частей - "Файлы" и "Метаинформация". Это нам создаст определённые проблемы с согласованностью данных (существование файла без метаинформации или наоборот). Однако, это будет не критично, если упорядочить операции добавления и удаления правильным образом - добавление файла, добавление меты, удаление меты, удаление файла. В этом случае проблема будет заключаться в том, что в случае ошибок, в S3 будут оставаться мусорные файлы, но если это вдруг действительно станет проблемой - можно будет достаточно безболезненно реализовать сборку этого мусора.
Все эти соображения в качестве примечания или описания ресурса стоит внести на диаграмму, если её целью является документирование реализации (т.е. планируются её долгий срок жизни или широкая аудитория). Я же эту диаграмму делаю для себя, чтобы спроектировать систему, оценить и спланировать работы. Поэтому не стану загромождать диаграмму этой информацией.
В итоге получаем финальную версию диаграммы эффектов проекта True Story в краткой нотации с событиями:
Заключение
Построение диаграммы эффектов уже дало нам много полезных штук:
Хорошее представление о том, что надо сделать - какие операции есть у системы и что они должны делать.
Список ключевых работ, которые необходимо выполнить для решения задачи - это можно взять за основу для оценки трудоёмкости работ.
Возможность увидеть часть реализации, в которой можно было легко ошибиться и избежать этой ошибки.
Интуитивно-понятную иллюстрацию для декомпозиции системы на модули - вы же тоже видите на диаграмме модули изображений, фида, интеграции с Яндексом и 2Гисом?
Но вернёмся к нашим изначальным вопросам:
Как оценить трудоёмкость задачи?
Как обеспечить низкую сцепленность системы?
В каком порядке реализовывать части системы и как эффективно распараллелить работу?
И ключевой вопрос - а что вообще надо сделать-то?
На последний вопрос мы получили исчерпывающий ответ.
Для ответа на первый вопрос у нас появились все входные данные и осталась только механическая работа по оценке простых и понятных блоков.
А вот ответов на второй и третий вопросы мы пока не получили. Для того чтобы их найти, нам необходимо декомпозировать систему на модули, о чём я напишу в следующем посте.