Создание новой системы — многоэтапный процесс: проработка концепции и дизайна, проектирование архитектуры, реализация, тестирование, релиз. Проектирование архитектуры и реализация — это те этапы, которыми в первую очередь занимаются разработчики.
Большинство разработчиков любят заниматься архитектурой, продумывать как система или её часть будет устроена с чистого листа. Если тот, кто продумал архитектуру системы, и будет её реализовывать, никаких проблем с мотивацией нет: программист получит удовлетворение от воплощения в жизнь задуманных им идей. Но если архитектуру продумал один, а реализацией будет заниматься другой, то у последнего может возникнуть естественное возмущение: все продумали за меня, а мне только делать по написанному?
О том, как избежать таких ситуаций, почему реализация может быть не менее интересной, чем проработка архитектуры, а иногда и более, речь пойдет в этой статье.
Введение
Продуманную архитектуру можно использовать как основу для breakdown'а задач: реализация каждого достаточно обособленного компонента становится отдельной подзадачей.
Например, если есть конвейер обработки запросов, архитектурно задуманный в стиле pipes & filters, то подзадачами будут реализация отдельных шагов обработки (на каждый шаг своя подзадача) и ещё одна подзадача для соединения всех шагов вместе.
Хотя продуманная архитектура и разбивка на подзадачи дают общее представление о том как сделать систему и позволяют оценить трудозатраты, их недостаточно для реализации задуманного. Описание подзадачи будет говорить, что должен делать компонент, возможно будет содержать требования по быстродействию и потреблению памяти, но не будет давать исчерпывающих инструкций о том, как его сделать.
Дело в том, что существует много вариантов сделать компонент, удовлетворяющий заданным требованиям. От того, каким образом он будет реализован, многое зависит: гибкость кода, его расширяемость, простота поддержки и т.д. Мы подошли вплотную к концепции дизайна кода.
Концепция дизайна кода
Иногда дизайн кода называют архитектурой или организацией кода, иногда даже просто архитектурой. Я придерживаюсь термина дизайн кода, потому что это противопоставляет его архитектуре системы и проводит между ними четкую границу. Чтобы быть конкретнее, рассмотрим пример.
Допустим, мы разрабатываем backend набирающего популярность сайта. Количество серверов уже перевалило за несколько десятков, аудитория растет, и мы решаем, что хотим собирать аналитику о поведении пользователей на сайте: популярность посещения страниц, частоту использования фичей в зависимости от профиля пользователя и т.п.
Возникает ряд архитектурных и технологических вопросов: где хранить метрики, как передавать их по сети, что делать в случае недоступности хранилища метрик, как backend-сервис будет записывать метрики и т.д. Архитектура как раз и должна отвечать на эти вопросы, определять компоненты решения и задавать требования к ним.
Допустим, мы выработали архитектуру: в качестве хранилища будем использовать InfluxDB, передавать метрики по сети с помощью UDP на telegraf, а для обхода проблемы недоступности хранилища будем складировать метрики в Kafka, реплицированную по нескольким серверам. Все метрики будут проходить путь backend-сервис -> telegraf -> Kafka -> InfluxDB. Для записи метрик backend'ом решили написать модуль, реализующий функционал передачи метрик в telegraf с помощью UDP.
Модуль для записи метрик — это отдельный компонент системы, его написание — отдельная подзадача, которую можно поручить разработчику. Эта подзадача имеет много вариантов решения и вопросов на которые нужно ответить: метрики будут отправляться синхронно или асинхронно; как будет синхронизироваться одновременный доступ нескольких потоков backend'а, какие будут основные классы/функции.
Эти вопросы лежат вне описания архитектуры решения, но ответы на них имеют далеко идущие последствия. Например, если в ходе эксплуатации решения станет ясно, что стек технологий выбран не оптимально и понадобится заменить telegraf на альтернативное решение, то неправильное разделение модуля на классы не позволит это сделать без переписывания всего модуля. Ответы на эти вопросы — вотчина дизайна кода.
Проработка дизайна кода — это отдельная стадия проектирования, которая находится между проработкой архитектуры системы и кодированием. Проведение границы между архитектурой и дизайном кода позволяют спроектировать систему без рассмотрения всех деталей и оценить трудозатраты за ограниченное время. С другой стороны, выделение проработки дизайна кода как отдельной стадии реализации позволяет поднять качество реализации системы, уменьшить затраты на дальнейшие доработки, увеличить простоту поддержки.
Необходимость продумывать дизайн кода на стадии реализации перед кодированием делает реализацию интересной: задачи проектирования дизайна кода могут быть не менее интересными, чем проектирование всей системы на уровне архитектуры. Эту мысль высказал еще Брукс в мифическом человеко-месяце.
Разумеется, провести границу между архитектурой и дизайном кода может быть не так просто, давайте рассмотрим этот вопрос подробнее.
Граница между архитектурой и дизайном кода
Идеологически архитектура и дизайн кода находятся на разных уровнях проектирования: архитектура продумывается на самой начальной стадии, когда мало определенности, а продумывание дизайна кода добавляет деталей. Соответственно они и выполняются в разные моменты времени: архитектура ближе к самому началу, а дизайн кода во время реализации подзадач.
Проведение границы между этими двумя стадиями проектирования зависит от ряда факторов, вот основные из них:
- Степень влияния компонента на систему. Иногда устройство всей системы может значительно зависеть от устройства её отдельного компонента. В этом случае проектировать компонент нужно на стадии проработки архитектуры, а не на стадии реализации.
- Наличие четкого интерфейса у компонента. Выделить проектирование компонента в подзадачу можно, только если четко определено, что должен делать этот компонент и как он будет взаимодействовать с остальной системой.
- Реалистичные оценки трудозатрат для выполнения подзадачи. Задача может быть слишком большая, чтобы можно было дать оценку трудозатрат с достаточной точностью. В этом случае лучше более детально спроектировать задачу и разбить её на собственные подзадачи, чтобы дать более адекватную оценку трудозатрат.
Есть несколько частных случаев, в которых можно хорошо провести границу между проектированием архитектуры и дизайна кода.
Компонент имеет строгое API
Например, в моей практике была задача: реализовать поверх UNIX-сокета API для захвата/высвобождения ресурсов ОС, используемых существующим демоном. Эта задача возникла в рамках выбранной архитектуры для новой epic-фичи. В рамках архитектуры было достаточно высокоуровнево описать API, а детальное проектирование было сделано позже, во время реализации.
Модуль/класс с заданным интерфейсом
Самый простой способ делегировать проектирование части монолитной системы — выделить некоторый модуль или класс, описать его интерфейс и решаемые задачи. Модуль, выделяемый в отдельную подзадачу, не должен быть слишком большим. К примеру, клиентская библиотека для доступа к шардированной базе данных несомненно является отдельным модулем, но задачу реализации этой библиотеки будет сложно оценить по трудозатратам без более детального проектирования. С другой стороны, задача реализации слишком маленького класса окажется тривиальной. Например, если возникает подзадача "реализовать функцию, проверяющую существование заданной папки по заданному пути", то архитектура явно продумана слишком детально.
Небольшой компонент с фиксированными требованиями
Если компонент достаточно малого размера и решаемая им задача строго определена, то оценить трудозатраты на реализацию можно с достаточной точностью, а сама реализация компонента будет оставлять простор в проектировании. Пример: процесс, запускаемый по крону и рекурсивно удаляющий старые файлы и директории по заданному пути.
Антипаттерны
Есть сценарии, когда распределение между продумыванием архитектуры и реализацией происходит неправильно, ниже рассмотрены некоторые из них.
Все спроектировано до мелочей
Построены подробные UML-диаграммы, задана сигнатура каждого метода каждого класса, описаны алгоритмы реализации отдельных методов… По такому детальному описанию можно реализовать системы быстрее всего, действительно, ведь все расписано до таких подробностей, что простора для творчества не остается вообще, бери и делай по написанному. Если цель в том, чтобы разработчик закодил то, что ему говорят, как можно быстрее, то да, можно так делать.
Однако если копнуть поглубже, то станет ясен ряд недостатков организации работы в таком ключе. Во-первых, чтобы спроектировать все в таких деталях, придется потратить большое количество времени на само проектирование. То, что обычно продумывает разработчик перед реализацией, в этой схеме будет продумывать архитектор: все проектирование смещается ближе к началу проекта, что может увеличить его продолжительность. Ведь если не разбивать работу по проектированию на части, то и распаралелить её не получится. Во-вторых, отсутсвие работы по проектированию во время реализации сильно снизит мотивацию разработчиков: делать ровно то, что говорят, может быть полезно для новичков, но опытным разработчикам будет скучно. В-третьих, такой подход может в целом снизить качество реализации на выходе: систему, не разбитую на достаточно независимые компоненты, будет сложнее поддерживать и расширять.
Архитектуру проектирует всегда один разработчик, остальные курят в сторонке только реализовывают
Прежде всего нужно отметить несколько кейсов, когда это может быть полезно. Во-первых, это команда, в которой много новичков и только один опытный программист. В этом случае у новичков недостаточно опыта для проектирования архитектуры, чтобы сделать работу достаточно качественно, в то же время реализация продуманной архитектуры поможет им поднять свой уровень. Во-вторых, это крупные проекты, в которые вовлечено несколько команд. Тогда проектирование архитектуры проекта делится на два уровня: архитектор продумывает её в целом, а каждая команда — архитектуру компонентов в рамках своей зоны ответственности.
Но рассмотрим одну команду, состоящую из достаточно опытных специалистов. Если архитектурные задачи всегда будут доставаться только одному, допустим, самому опытному разработчику, то остальные разработчики не смогут в полной мере раскрыть свои возможности. Архитектура системы будет однобокой, ведь у каждого есть набор приемов, которые он применяет. Если бы архитектуру разных компонентов / подсистем продумывали разные разработчики, это способствовало бы обмену опытом и развитию членов команды. Даже не слишком опытным членам команды стоит иногда давать архитектурные задачи: это позволит поднять их уровень и увеличит их вовлеченность в проект.
Заключение
Наличие в реализации стадии проектирования — основной фактор, который делает задачи на реализацию интересными. Конечно, есть и другие: применение новых технологии, исследовательские задачи, однако они, как правило, встречаются значительно реже. Если задачи на реализацию не будут требовать проектирования и будут заключаться в простом кодировании, то это сильно ударит по мотивации разработчиков и не позволит использовать их умения и навыки.
Проектирование дизайна кода на стадии реализации позволяет быстрее делать адекватные оценки трудозатрат, эффективнее распараллеливать работу и в целом повышает качество системы.
Необходимость проектировать дизайн кода во время реализации — это именно то, что делает работу по реализации интересной в глазах разработчиков.
Не стоит совершать ошибок, исключая работу по проектированию из реализации подзадач, как и не стоит всегда поручать архитектурные задачи только самому опытному разработчику.
Автор: vadimnt