Когда я был разработчиком я задавался вопросами:

  • как разделить код на классы?

  • какие модули выделить?

Когда я стал архитектором я задавался вопросами:

  • зачем же мы наплодили 200 микросервисов?

  • стоит ли выделять новый или пора объединять?

Когда я стал руководителем я задавался вопросами:

  • как разделить людей на команды разработки?

  • стоит ли создавать новый отдел или расширить ответственность старого?

И всё это хотелось сделать оптимальным эффективным образом.

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

Как я принимал подобные решения пока не вывел единые принципы?

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

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

  • Если у меня на руках что-то крупное, значит нужно делить 

Моя проблема была в том, что я мыслил понятиями хорошо и плохо, идеальное и неидеальное. Каждое решение тем не менее давалось тяжело, вызывало много размышлений и оценку разных факторов, и была скорее похоже на вытаскивание кролика из шляпы после долгого блуждания в тумане (только вместо шляпы использовался мозг и опыт, откуда магическим образом доставались и сразу принимались решения):

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

  • Стоит ли выделить под это отдельную команду разработки? Чуйка подсказывает, что платформенная команда делающая низкоуровневые инструменты это правильно.

  • Стоит ли выделять сервисы для продуктовой логики? Чуйка подсказывает что в текущей ситуации нужно.

И со временем я начал понимать, что мои решения могли быть лучше. А уже принятые стоили компании лишних денег. Как говорилось в книге Fundamentals of Software Architecture:

"Нет неверных ответов, есть дорогие."

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

Цели цели и еще раз цели

Пример про шкафы

Представь, что ты (один или с семьей) въехал на съемную квартиру, где есть 2 шкафа с одеждой. Каким образом ты оптимальным образом распределишь одежду по шкафам?

  • Может быть верхнюю одежду в один, а нижнюю в другой?

  • Может быть будничную в один, а выходную в другой?

  • Может быть взрослую в один, а детскую в другой?

  • Может быть всё в один шкаф?

  • Может быть просто оставишь все в чемоданах? 

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

Я понял, что даже чтобы разложить одежду по шкафам мне нужно понимать:

  • а зачем я черт возьми это делаю? может просто побросать всё на пол?

  • а зачем мне вообще 2 шкафа, может разложить всё в 1?

  • в каких случаях 2 шкафа облегчат мне жизнь?

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

Я же, отвечая на вышестоящие вопросы пришел к выводам:

Чтобы выбрать оптимальное решение из нескольких вариантов необходим:

  • Набор кейсов, которые тебе приходится решать

  • Оценка усилий, которые ты х��чешь минимизировать

Это критически важная часть продумывания задачи, так как заставляет нас думать о целях. Здесь мы отвечаем на вопрос, а ЗАЧЕМ мы вообще что-то делаем? Возможно задача не стоит затраченных усилий.
В данном случае усилия можно измерить реальным количеством физических действий, количеством шагов, которые нужно совершить, чтобы выполнить свои повседневные задачи.

Примеры того как кейсы меняют задачу:

  • Если приехали на съемную квартиру на 1 день только переночевать и наши кейсы это раздеться вечером и одеться утром. Возможно мы потратим больше сил на раскладывание по шкафам и лучше просто оставить все в чемоданах.

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

  • Возможно вы немного психопат и ваша цель каждый день надевать одежду одного цвета.

Представим, что у нас обычный случай, когда въезжают 2 взрослых человека. Тогда ваш кейс – быстро и удобно одеваться. Максимальное количество усилий мы тратим, когда нам приходится ходить в разные шкафы, чтобы реализовать 1 кейс одевания. Это некая совсем аварийная ситуация, говорящая нам о том, что мы накосячили с разделением. 

Итак, мы теперь реально понимаем чего хотим достичь:

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

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

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

Вот какие рассуждения возникают:

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

  • Согласно нашим критериям абсолютно оптималь��ым было бы просто положить все вещи в 1 шкаф и проигнорировать остальные. Но у нас слишком много одежды, поэтому думаем дальше.

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

  • У нас не существует кейсов, когда нужно одновременно надевать мужскую и женскую одежду, поэтому мы просто разложим мужскую и женскую отдельно. И места в шкафу хватает. Это и будет оптимальным решением под наши кейсы.

✅ Стоит также учесть, что если у нас 10 шкафов, а не 2, то решение абсолютно никак не меняется и остальные 8 можно просто продать/проигнорировать, так как любое их использование только усложнит работу.

Правило 1 - не дели без необходимости

Посмотрим еще раз на вопросы заданные в начале:

  • Как разделить код? 

  • Как разделить микросервисы?

  • Как разделить команды разработки?

  • Как разделить оргструктуру?

Оптимальный случай – когда деление не нужно. Не дели без необходимости. 

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

  • Деление на шкафы может заставить нас тратить усилий на хождения между ними. Оно имеет смысл, только если в шкаф в не влезает одежда.

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

  • Деление на уровне микросервисов может усложнить разработку, уменьшить производительность, привести к проблемам с консистентностью, итд.

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

Это стало моим дефолтным решением - не делить. Не создавать модуль, не создавать команду, не создавать отдел. Пока не наступит осознанная необходимость.

Правило 2 - дели под кейсы, оптимизируй усилия

Под кейсам в идеальном варианте я здесь понимаю ВСЕ (тысячи, миллионы) конкретных задач, которые реалистично нужно решить с помощью делимого объекта. Конечно описать их все невозможно, но чем лучше ты будешь их представлять (хотя бы в обобщенном виде, сформулированном в виде поведения, стратегии, направлений) тем лучше. 

  • Нет смысла оценивать или принимать решение, не понимая зачем (под какие кейсы и для решения каких проблем на каком периоде) оно было сделано.

  • Чем лучше ты понимаешь скоуп задач, которые будут стоять перед тобой, твоим заказчиком, твоим отделом, твоей компанией в ближайшем будущем, тем лучше ты сможешь принять / оценить решение.

Как и в примере со шкафами, когда некоторые люди моментально давали оптимальный ответ, многие при определении кейсов действуют интуитивно, на опыте. Часто при отсутствии других источников, именно опыт подсказывает какие задачи потенциально должно решать:

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

  • ага, здесь мы создадим еще одну команду разработку, потому что одна команда по моему опыту точно не справится

Именно поэтому от опыта сильно зависит сложность реализации:

  • начинающий программист напишет просто, потому что не умеет сложнее

  • средний программист напишет сложно по всем мыслимым правилам

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

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

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

Примеры использования правил

Пример 1: Создаем команды разработки

Ты проектируешь структуру команд разработки департамента. Следуем правилам:

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

  • Понять что является критерием для оценки. Для нас это эффективность команд. Внешний и внутренний опыт показывает, что эффективные команды прежде всего автономны: имеют независимый беклог, владеют собственным набором систем. Любая межкомандная задача бьет по скорости разработки в 10 раз. (https://martinfowler.com/articles/talk-about-platforms.html , https://teamtopologies.com/). Таким образом, по аналогии со шкафами, аварией / неэффективностью мы считаем случаи когда для реализации 1ой задачи необходимо привлекать больше 1 команды. 

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

  • взять все задачи которые потенциально упадут на оргструктуру 

  • разделить команды так, чтобы минимизировать % межкомандных задач (автономность = эффективность)

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

Пример 2 - делим приложение на сервисы

Ты разрабатываешь микросервисную архитектуру и размышляешь стоит ли разделять 1 сервис на несколько более мелких для эффективности разработки. Поделить одно и то же можно совершенно разными способами:

Из Википедии по "Микросервисной архитектура": Философия микросервисов фактически копирует философию Unix, согласно которой каждая программа должна «делать что-то одно, и делать это хорошо» и взаимодействовать с другими программами простыми средствами: микросервисы минимальны и предназначаются для единственной функции.

Это понятие очень размыто. Что считать единственной функцией? Можно поделить на 100 мелких функций или на 1 большую бизнес функцию. Разные люди проведут границы по-разному. Какой вариант лучше? Почему именно тот что выбрали вы является оптимальным?

Чтобы это понять следуем всё тому же алгоритму деления, которые вывели ранее:

  • Понять поток кейсов.

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

    • Критически важным также является рассмотреть нефункциональные требования (требуемые архитектурные характеристики). Примеры можно взять из стандарта ISO 25010 - System and software quality models. Там есть интересные свойства, такие как модульность или переиспользуемость, которые тоже являются требованиями к качеству. 

Но по сути стандартами пользоваться не обязательно. Любая важная архитектурная характеристика может быть учтена здесь. Вот забавный пример когда не потерять коммуникацию с Италией стало важной характеристикой Italy-ility:

ITALY-ILITY
One of Neal’s colleagues recounts a story about the unique nature of architectural characteristics. She worked for a client whose mandate required a centralized architecture. Yet, for each proposed design, the first question from the client was “But what happens if we loose Italy?” Years ago, because of a freak communication outage, the head office had lost communication with the Italian branches, and it was organizationally traumatic. Thus, a firm requirement of all future architectures insisted upon what the team eventually called Italy-ility, which they all knew meant a unique combination of availability, recoverability, and resilience.

Вот еще пример архитектурных характеристик из книги Fundamentals of Software Architectre:

Выяснять архитектурные характеристики сложно. Бизнес редко (никогда) не формулирует в четком виде эти требования, но часто можно перевести с бизнесового на технический язык. Например:

  • когда в стратегии говорится, о том том что мы будем покупать много компаний (M&A - Mergers and acquisitions), то вероятно речь идет о

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

    • Scalability. Масштаби́руемость — способность системы, сети или процесса справляться с увеличением рабочей нагрузки (увеличивать свою производительность) при добавлении ресурсов (обычно аппаратных).

    • Extensibility — способность расширять систему и наращивать новый функционал. 

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

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

Разные требования к архитектурным характеристикам могут быть причиной разделения на архитектурные кванты (микросервисы в нашем случае)

Следует стремиться не раздувать количество характеристик, например дать заказчику выбрать наиболее 3 важные из списка. В противном случае можно получить очень дорогую архитектуру (бесконечная гибкость = бесконечная сложность).

Теперь выполним второе действие из алгоритма деления:

  • Нужно понять что является критерием для оценки. Есть базовый принцип - Low coupling, High Cohession. Таким образом аварией мы считаем когда одно требование или одна решаемая задача затрагивает несколько разных сервисов. Ситуация существенно ухудшается если эти сервисы принадлежат разным командами. В этом случае доработка будет максимально дорога, потому что будут задействованы несколько структур (межкомандная задача это 10х по скорости реализации)

Именно команда и взаимоотношения между ними являются критичным базовым архитектурным кирпичиком, напрямую влияющим на построение технических сервисов (подробно об этом в Teams Topologies). Именно неправильное деление на команды, например создание платформенной команды, может создать неоптимальную структуру сервисов и усложнить разработку.

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

В этом случае необходимо:

  1. убедиться, что необходимость в разделении есть

  2. посмотреть / представить все решаемые бизнесом задачи, связанные с рассматриваемыми IT сервисами и командами. 

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

  4. вернутся к первому пункту, переходя к уже поделенному блоку

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

Например отделение сервиса (и любая Distributed архитектура) сразу провоцирует набор проблем:

  • задержка на взаимодействие с сетью: проблемы с bandwidth, проблемы с сетью. Насколько ухудшится производительность? Производительность вполне может быть важной для нас архитектурной характеристикой.

  • сам сервис стоит денег: сервера, прокси, деплои, настройка доступов, развертывание на тестингах

  • невозможно выполнить одну транзакцию в БД

  • eventual consistency - задержка на обновление данных

  • сложнее обрабатывать ошибки если сервисы связаны

  • необходимость поддерживать контракты и версионирование

  • и другие

Действительно ли оно того стоит? Можно ли обойтись без деления? Возможно можно обойтись разделением на уровне кода?

Часто люди говорят, что разделение на сервисы помогает им лучше соблюдать границы. Но оптимальны ли эти границы под кейсы? Есть мнение, которое я поддерживаю, что если ты не можешь написать нормальный монолит, то микросервисы не помогут. Ничто не мешает провести неправильно границы в микросервисах, а исправить их существенно сложнее чем на уровне кода. Неправильно распилив, можно получить распределенный монолит и только существенно усложнить ситуацию и получить так называемую Accidental complexity (неоправданную сложность). Большинство разработчиков очень любят создавать сложные системы, так как это интересно, об этом пишут статьи, это хороший опыт и прокачка знаний, у этого благая цель. Проблема только в том, что всё это стоит компании денег и стелить соломку везде очень дорого.

Developers are drawn to complexity like moths to a flame—frequently with the same result.

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

Пример 3 - Деление на уровне кода

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

  • с одной стороны каждый класс должен иметь 1 причину для изменения

  • с другой можно объединять разные ответственности, если это удобно поддерживать

"Следование принципу единственной ответственности зависит от функций программного продукта и является труднейшим при проектировании приложений. "

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

  • Понять поток кейсов. Что именно с большой долей вероятности будет меняться отдельно? Какой функционал нужно в ближайшее время будет развивать? Источниками опять же может быть стратегия и планы компании, департамента, команды, опыт прошлого.

    •  Чем лучше ты понимаешь задачи, которые стоят перед бизнесом, тем лучше ты спроектируешь структуру классов. 

      • Нельзя сделать гибко под сразу все кейсы. Бесконечная гибкость = бесконечная сложность.

      • Нужно подкладывать соломки под обоснованно известные кейсы и планы бизнеса.

  • Понять что является критерием для оценки. SOLID как раз предлагает считать аварией, если для для реализации одного требования необходимо изменять больше 1 класса.

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

Общий алгоритм деления

  • Максимально собери все реалистичные кейсы, связанные с рассматриваемыми блоками, которые предстоит решить 

  • Выбери как ты будешь оценивать эффективность деления (best practices, опыт, понимание что хочет руководитель структуры и его руководитель).

  • Убедись, что есть острая необходимость в разделении. С чем не справляется текущая структура. Запиши минусы от разделения (они точно есть). Старайся обойтись минимальным количеством блоков.

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

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

  • Выбери решение с максимальной эффективностью

  • Оцени сколько усилий потребуется на само разделение. Убедись (и убеди стей��холдера), что выгода от реализации стоит затраченных усилий.

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