В прошлой части мы разобрали:

  • что техническая реализация заметно влияет на успехи бизнеса, хоть и не очень критично;

  • что из всех аспектов технической реализации наибольший вклад в успех вносит именно архитектура;

  • что самое важное свойство архитектуры - максимальная независимость команд друг от друга;

  • что это свойство вытекает напрямую из двух фундаментальных характеристик программного обеспечения: coupling и cohesion;

  • где coupling - характеристика связи двух точек системы/кодовой базы;

  • а cohesion - характеристика того, насколько плотно упакованы такие связи в компоненты.

С этим багажом заканчиваем с теорией и переходим к практике.

И что нам теперь делать со всеми этими знаниями?

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

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

  • Как не допустить появления связанной архитектуры и сразу сделать хорошо?

  • Как исправить уже связанную архитектуру?

В этой части постараюсь развернуто ответить именно на первый, оставив второй на десерт.

"Что такое эволюционный дизайн" или "где накосячил ваш архитектор"

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

Мы так часто видим плохие архитектуры и ругаем их за непродуманность и костыли, но обратите внимание, что, как правило, почти все архитектуры, какие бы вы ни встретили, как раз, были очень тщательно продуманы. И проблема этих архитектур отнюдь не в непродуманности. Их беда в том, что нарисованные компоненты и стрелочки на схеме - это одно, а реальные связи в системе - это совсем другое. И вот эту мысль я вам предлагаю запомнить.

Классика жизни
Классика жизни

Это очень важный вывод, к которому пришли лидеры нашей индустрии еще в далекие семидесятые. Каким бы талантливым архитектором вы бы ни были, вы не угадаете. И с тех пор по этим граблям десятилетиями с упоением скачут как новоиспеченные, так и умудренные опытом архитекторы и техдиры по всему миру. В итоге, мы регулярно видим системы, где связи, вместо того чтобы группироваться плотными пучками, ровным слоем размазаны по компонентам, придуманным задолго до разработки.

Есть ли способ угадать с архитектурой?

Если честно, есть, но очень приблизительный. Возьму близкий мне пример: если вы пишете софт, скажем, для банка, то очевидно, что выдача кредитов и денежные переводы - это достаточно далекие друг от друга вещи.

Так почему это не решение? Проблема в том, что, если вы хотите уложиться хоть в какие-то сроки, то вам потребуется бросить в работу много людей. Это значит, что уже в рамках этих понятий вам вновь потребуется разбиение на команды. И вот тут-то и кроется дьявол: чем детальнее разбиение, тем выше риск не угадать. Как правильно разделить домен "денежные переводы" на 10 команд? Не знаете? Вот и никто сразу не знает.

А как иначе?

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

  • Как тогда нарезать компоненты?

  • Зачем тогда их нарезать вообще?

Начнем с последнего.

"Обратно в монолит" или "зачем нужны микросервисы".

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

Дежурный мем про микросервисы.
Дежурный мем про микросервисы.

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

Про микросервисы в рамках одной команды наглядно
Про микросервисы в рамках одной команды наглядно

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

Может, тогда резать софт на команды и компоненты вовсе и не надо?

Человеческая природа против архитектуры и проектного менеджмента

Действительно, если вы работаете дружным коллективом в 5-10 человек, то по-настоящему веских причин резать ваш софт на отдельные части у вас не очень много. Разве что это ну совсем отдельные домены.

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

Разница между временем разработки большой командой и небольшой командой для среднего проекта из 100 000 строк кода составляет примерно 1 календарную неделю (8,92 месяца против 9,12).
Разница между временем разработки большой командой и небольшой командой для среднего проекта из 100 000 строк кода составляет примерно 1 календарную неделю (8,92 месяца против 9,12).

На самом деле, такие большие команды все равно в итоге самостоятельно разбиваются на небольшие подгруппы. И обычно размер таких подгрупп следует с одной сторны из природы человека, с другой - из математики. А именно из квадратичной зависимости числа связей от количества элементов в группе. То есть, если в команде из 4 человек будет всего 6 связей, то в команде из 10 человек уже 45 связей.

И чем больше связей, тем тяжелее нашему мозгу их поддерживать. Из социологии известно, что люди поддерживают близкие связи, как правило, в коллективах численностью не более 15 человек. Обычно меньше, поэтому базовая рекомендация из умных книжек - не более 5-9 человек на команду.

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

Отсюда вывод: лучше иметь много маленьких команд, чем одну большую. Значит, ответ на вопрос, нужно ли резать коллектив на команды: да, конечно, нужно. Но нужно ли резать софт на компоненты?

Про много команд на один сервис

Выше я упоминал ситуацию, где одна команда поддерживала 70 сервисов, но что если будет наоборот? Могут ли много маленьких команд эффективно работать на одной кодовой базе?

Ответ на этот вопрос кроется в исследовании, упомянутом в начале моего повествования. Главный критерий успеха работы команды - это её независимость от других команд. Если вы способны обеспечить командам в монолите независимость проектирования, разработки, тестирования и развертывания, то, наверняка, они смогут работать и на одной кодовой базе.

Есть немало известных крупных компаний, где исторически сложившийся монолит десятилетиями не отменял независимости команд. В их числе Facebook, Twitter, Gitlab и многие другие компании, которые долгие годы использовали модульные монолиты для разделения работы между командами. В таких монолитах, как правило, каждая команда все равно работает с небольшим независимо разворачиваемым и тестируемым компонентом, который имеет право изменять только она. В итоге, от классического микросервиса такой компонент отличается только механизмом развертывания.

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

Преимущества и недостатки работы на одной кодовой базе в сравнении с корректно реализованной микросервисной архитектурой.
Преимущества и недостатки работы на одной кодовой базе в сравнении с корректно реализованной микросервисной архитектурой.

Итого: мы не можем оставить одну большую команду и не можем угадать с разделением на компоненты на старте. Как тогда быть?

Эволюционный дизайн или как все-таки делить ПО на компоненты

Вот мы обсудили результаты исследований, базовую теорию и как делать не надо. Пора уже двигаться к "как надо".

Давайте, теперь я еще раз сформулирую проблему.

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

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

Когда вы начинаете, вы знаете о проекте меньше всего, а когда заканчиваете, вы знаете о нем больше всего. [Tom McFarlin]

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

Хорошая архитектура позволяет вам отложить принятие важных решений, таких как пользовательский интерфейс, фреймворки, база данных и т.д. [Uncle Bob]

Такой подход называется эволюционным дизайном или, иначе, непрерывным дизайном (continuous design). В этой концепции вы не разрабатываете архитектуру заранее - вы разрабатываете и перерабатываете её всегда, в любой момент времени.

Эволюционный подход держится на 3 китах:

  • Планировать заранее архитектуру бессмысленно из-за низкой вероятности угадывания основных проблем.

  • Продолжительное предварительное планирование (обычно, непродуктивное, потому что см. пункт выше) отнимает у разработки драгоценные недели и месяцы, сдвигая, таким образом, и время доставки вправо.

  • Команды и архитектура должны формироваться в процессе разработки на основе складывающейся обстановки.

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

Эволюционный дизайн работает гораздо лучше заранее тщательно продуманного дизайна. Большинство информационных систем (Гугл, Телеграмм, Яндекс, Нетфликс), которыми ежедневно пользуются люди, были разработаны и до сих пор развиваются именно так.

Техника безопасности эволюционного дизайна.

Конечно, надо понимать и ограничения такого подхода.

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

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

    В запланированном дизайне обычно считается, что архитектура сразу более-менее верна и в больших корректировках не нуждается. И это, конечно же, чушь собачья: чем больше было нафантазировано заранее, тем больше потом нужно будет исправлять.

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

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

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

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

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

    Пишите тесты всякие, не только юниты.
    Пишите тесты всякие, не только юниты.

    При этом и я, и коллеги из DORA (стр 53), и другие лидеры индустрии - мы все рекомендуем:

    • Сделать сьют автотестов настолько полным, чтобы быть уверенными, что, если тесты зеленые, то ничего не сломано.

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

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

    Нередко на старте свободных людей бывает больше, чем на одну команду, а сроки поджимают. На что ориентироваться, если делить уже надо, а понимания четкого нет? Надо смотреть на предметную область, пользователей, на сценарии, профиль нагрузки, регуляторные особенности и частоту изменений в том или ином месте. На эту тему рекомендую ознакомиться с главой Fracture Planes в книжке Team Topologies.

    Например: очевидно, что, скажем, кредиты, страховки, переводы и сбережения - разные предметные области банковского дела. Скорее всего, физлица, юрлица и сотрудники банка даже в одной и той же области будут иметь очень разный функционал. Сценарии перевода средств через СБП и через Центробанк настолько разные, что можно довольно смело разделять их на разные команды.\

    Тут я должен сделать оговорку. Если вы работаете в банке, то вы прекрасно понимаете, что эти примеры - натяжка. И чаще всего вопрос стоит еще сложнее: как нарезать ответственности на команды уже внутри таких доменов? Могу посоветовать правило: попытайтесь нарезать вашу систему на вертикальные срезы так, чтобы эти срезы минимально контактировали друг с другом.

    Другой пример разделения - профиль нагрузки. Например, потоковая/массовая обработка. Допустим, есть непрерывное проведение большого потока данных (платежи, заказы, запросы, заявки) и есть работа с накопленными данными по платежам: интерфейс пользователя или отчетность. Это очень различающиеся между собой сценарии работы, поэтому там будут разные механизмы оптимизации производительности даже при очень похожих моделях данных. И, как следствие, очень разные технические реализации.

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

  6. Следующий нюанс: допустим, одна из команд явно оказалась избыточна для выделенной под неё работы. Справедливо ли утверждение, что одна команда должна поддерживать только 1 компонент? И да, и нет.

    Правильный вопрос тут скорее другой: сколько отдельных предметных областей может поддерживать одна команда? В книжке Team topologies рекомендуется ограничивать когнитивную нагрузку на команду так, чтобы одна команда поддерживала не более 1 сложной предметной области. Это такой домен, где ничего не понятно и требуются недели на то, чтобы разобраться. С другой стороны одна команда может владеть несколькими простыми предметными областями, где ничего не надо анализировать - все понятно и так.

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

  7. И последнее: а что если вы не на старте? Что если вы уже обнаружили себя в ситуации, где у вас все поделено, но границы явно проведены не там, где надо? Об этом, собственно, мы и поговорим в следующей части повествования.


Меня зовут Саша Раковский. Работаю техлидом в расчетном центре одного из крупнейших банков РФ, где ежедневно проводятся миллионы платежей, а ошибка может стоить банку очень дорого. Законченный фанат экстремального программирования, а значит и DDDTDD, и вот этого всего. Штуки редкие, крутые, так мало кто умеет, для этого я здесь - делюсь опытом. Если стало интересно, добро пожаловать в мой блог.

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


  1. alexhott
    18.06.2025 05:20

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

    Основной вывод который я сделал - всем команды должны придерживаться общих требований к архитектуре, изменения к этим требования должны вовремя доводиться до всех команд. Тогда разработка будет более эффективной, меньше косяков и быстрее.

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