С момента написания предыдущей статьи я находился под пристальным вниманием. Попытка опубликовать материалы на англоязычных платформах обернулась фиаско — в первые же минуты легионы последователей тайного братства обрушились с критикой:

— Нет никакой организации! — вопили они.

Подозреваю, что слежка велась через мой телеграм-канал.

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

Заговор разработчиков разоблачается в трёх статьях:

  1. Тактика работы с кодовой базой;

  2. Тактика работы с архитектурой (эта статья);

  3. Тактика работы с коллективом (еще не опубликована).

Тактика работы с архитектурой

Чего бы нам хотелось получить от архитектуры проекта глобально? Предельно упрощая, можно выделить 2 требования:

  1. Хотим соблюдать SLA (время ответа сервиса, RPS, минимальный latency, uptime и т.п.)

  2. Хотим, чтобы фичи быстро релизились (Time to market, TTM).

Остальные хотелки — производные. Например, хочется, чтобы сложность кодовой базы росла как можно медленнее. Но зачем? Чтобы сократить TTM.

Хочется, чтобы решения масштабировались, но зачем? Чтобы SLA соблюдать, когда аудитория сервиса продолжит расти.

Хочется, чтобы нас не могли «задедосить», но зачем? Чтобы соблюдать SLA (Uptime guarantee).

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

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

Бойтесь идолов!

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

И́дол (греч. εἴδωλον, др.-рус. идолъ; также, кумир, фантом, истукан) — материальный предмет, который служит объектом религиозного поклонения и магических действий. — Wikipedia.

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

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

Иди, вера твоя спасла тебя. И он тотчас прозрел и очистил себя и код свой. — Потерянные стихи из Техно-Евангелия братства технического угнетения.

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

  • к спорам на code review со ссылками на авторитеты;

  • к потере времени на чтение очередной статьи о том, как действительно нужно трактовать эти принципы;

  • к отвлечению от написания простого работающего кода;

  • к ограничению восприятия (делай так, а не иначе, независимо от контекста).

Что же это за принципы, о которых идет речь? Вы готовы узреть ложь в одеянии правды?

Это принципы, сформулированные изначально под аббревиатурой IDOLS!

I.D.O.L.S!

I.D.O.L.S!

Но это слово известно и под другим именем

Бойтесь этого слова, ведь оно суть S.O.L.I.D!

Позволю себе сослаться на так удачно вышедшую статью «Перестаньте молиться на принципы S.O.L.I.D». Согласен со всем, кроме примеров Liskov Substitution и Interface Segregation — там, на мой взгляд, критикуемый код с интерфейсами выглядит лучше.

В статье не было примера необязательности Dependency Inversion Principle, приведу свой. Возьмём сущность ProductsDataSource. Вместо того, чтобы выделять интерфейсProductsDataSource и реализацию SomeProductsDataSourceImpl, можно оставить ProductsDataSource единственной реализацией.

Было:

class ProductsDataSource()

fun someCodeOne(productsSource ProductsDataSource) {
    // ...
}

// ...

fun someCodeN(productsSource ProductsDataSource) {
    // ...
}

Если понадобится другая реализация, просто заведите интерфейс ProductsDataSource, а единственную реализацию переименуйте в SomeProductsDataSourceImpl — никакие изменения в других местах не нужны, всё теперь завязано на интерфейс.

class ProductsDataSourceImpl()

interface ProductsDataSource {}

// остальное не меняется

Хотелось бы добавить и про Opened/Closed. Боб в своих лекциях критикует switch внутри функции и приводит преимущества решения через расширение классов. Проблема тут в том, что такой подход работает только при добавлении новых классов. Если же постоянно добавляются новые функции, нам придется изменять уже написанные классы, нарушая этот же самый O/P. Проблема известна с 1975 под именем expression problem.

S.O.L.I.D. — это всего лишь набор случайных принципов, применение которых более или менее актуально в зависимости от контекста задачи и языка программирования.

Злоупотребление DRY

Можно оправдать любое злодеяние, если подобрать нужные слова. Одно из таких слов — DRY (Don't Repeat Yourself).

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

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

Принцип DRY впервые был сформулирован в 1999 (в книге «The Pragmatic Programmer»), когда разработчики не имели возможности открыть GitHub, посмотреть Issue, PR и все измененные файлы. Не было умных редакторов кода, которые с LLM или другой реализацией паттерн-матчинга позволили бы пробежаться по коду и сделать автозамену. Не было решений, вроде sealed class в Kotlin и Java с исчерпывающим when и switch (с 17 версии).

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

Первая проблема DRY — код становится менее читаем. Чем больше переходов нужно, чтобы добраться до логики сквозь дебри абстракций, тем сложнее. В этом плане можно смотреть на DRY как на меру нарушения другого принципа — LoC (Locality of Behaviour), облегчающего восприятие кода.

DRY vs WET (We Enjoy Typing, Write Everything Twice)
DRY vs WET (We Enjoy Typing, Write Everything Twice)

Вторая проблема DRY — возможность внести ошибку при изменении общего кода, когда только в подмножестве нужны изменения. Допустим, была функция, возвращающая сущность User из базы, а затем ей добавили колонку deleted. В большинстве мест нужно отфильтровать по признаку deleted, но не везде, а программист, меняющий код, может об этом не подумать и добавить изменение только в общий код. Как результат — неявные баги, которые могут обнаружиться на проде.

Третья проблема DRY — связанность всего со всем. Как в старом анекдоте про ООП: захотели банан — получили обезьяну, держащую этот банан, и джунгли в придачу. И проблема не только в отсутствии возможности в будущем безболезненно вынести часть кода в отдельный сервис. Связный код сложнее тестировать и сложнее рефакторить, что напрямую влияет на TTM. Если начать говорить об этом с техно-анархистом, можно услышать про high cohesion, low coupling. Делается это для того, чтобы уйти от необходимости обсуждения проблем по существу в софистику, ведь low coupling может быть определен субъективно, а в реальных приложения и вовсе неосуществим:

High coupling must be unavoidable, statistically speaking, apparently contradicting standard ideas about software structure. - research paper, Can We Avoid High Coupling?

Четвертая проблема DRY проявляется в крупных проектах, если общий код (какой-нибудь набор common_utils) между двумя сервисами или приложениями выносится в отдельный репозиторий. Теперь, чтобы зарелизить задачу, нужно сперва зарелизить библиотеку с общим кодом. Будет два независимых пулреквеста. Разные релизные ветки будут зависеть от разных релизов библиотеки. Придется думать об обратной совместимости. Как результат, TTM возрастет на порядок.

Не призываю отказываться от DRY, призываю не злоупотреблять. DRY хорош, но лучшее — враг хорошего. Low coupling — тоже хорошо, но 0 coupling — это бесполезный код, который можно удалить.

Решения, убивающие скорость получения обратной связи

The quicker I get the feedback, the happier I am. — Martin Fowler

У Мартина Фаулера наберётся не один десяток высказываний про важность петли обратной связи. В том числе про то, что не всё можно спроектировать сразу, и что существует «степень понимания, которая достигается только выполнением программы, когда можно увидеть, что действительно работает»

Безусловно, есть задачи, которые на языках со статической типизацией выполняются «с первого раза» — написал, запустил, работает. Это как правило какая-то стандартная рутина.

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

Выглядит это так:

  1. Разработчик делает ПР (потому что локально не воспроизводится).

  2. Ждет CI 45 минут и получает очередную ошибку.

  3. Думает над новым решением и Возвращается к 1.

За день так можно сделать около 8-10 попыток. Если же потратить полчаса (полдня) и добиться воспроизведения проблемы локально, можно сделать те же 10 попыток за 10 минут.

Уничтожители корпораций — опытные разработчики, поэтому легко могут найти способы замедления петли обратной связи на проекте, чтобы не попасть под подозрение в саботаже. Например, выбрать библиотеку для DI (вроде Dagger 2), использующую кодогенерацию. На больших проектах каждый git pull будет сопровождаться чистой сборкой.

Особенно легко внедрить Dagger в мобильной разработке, где можно давить на важность быстрого старта приложения для обоснования DI с кодогенерацией. И не важно, что для большинства девайсов это вопрос 0.45-20ms (замеры из прошлого, уверен, на новых моделях еще быстрее).

Есть проекты вроде dagger-reflect, чтобы в compile time была кодогенерация, а во время разработки — reflection, но техно-террористы каким-то образом отвлекли Джейка Вортона от проекта.

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

В Android-разработке феноменально хорошо себя зарекомендовал robolectric — как инструмент для выключения TDD. Тесты будут запускаться так долго, что никто их запускать не будет.

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

Монорепозитории и гравитация

Как мы уже проговорили выше, для счастья и продуктивности разработчика критически важна скорость получения «фидбека». Соответственно, витязи технического упадка хотят организовать монорепозиторий так, чтобы обратную связь разработчик получал как можно медленнее. Давайте сразу введем термин «гравитации репозитория» — то, насколько время решения задач замедляется в результате плохой организации проекта.

Лид монорепозитория
Лид монорепозитория

Пример из опыта. Я работал в российском «FAANG» в 2021, когда лид мобильной разработки решил прошить «гредловую» нить сквозь все gradle-репозитории. Последние уже были частью «монорепы», но оставались свободными — то есть имелась возможность открыть в редакторе кода только нужную папку и работать с ней, независимо от остальных.

Я жил счастливо и буквально парил над землей, ведь гравитация не ощущалась — сборка проекта занимала 2-5 секунд, clean build — секунд 20-30. Добавление новой зависимости и синхронизация системы сборки — секунд 10-15.

Вернувшись из отпуска, я узнал, что репозиторий переехал на «multi project builds». Под тяжестью гравитации вся команда оказалась на лопатках — время сборки проекта увеличилось в 5-10 раз. До меня дошли слухи, что кто-то (возможно, тот самый лид) потерял 3 часа из-за того, что версии библиотек у двух репозиториев не совпали. Чтобы такое не повторилось, было принято решение, из-за которого по 3 часа в день теперь терял каждый из 50+ человек .

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

Также не могу не добавить, что бывший коллега, ревьюивший статью, матерился только в этой части текста: «я это *** *** по 40 минут синкал ***» — написал потерпевший.

Переход на микросервисы без нужды

Ибо истинно говорю вам: если вы будете иметь веру с горчичное зерно и скажете монолиту сему: «перейди на микросервисы», и он перейдёт; и ничего не будет невозможного для вас; — Потерянные стихи из Техно-Евангелия братства технического угнетения.

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

«Как продать микросервисы: пошаговый план для разработчика» (Москва, Highload++ 2024).

Давайте сразу разделим понятия микросервисы и сервисы (service-based architecture). Сервисы — это разбитый по доменным областям монолит (например, сервис доставки). Микросервисы — уже разбитые по функциям сервисы (например, микросервис нотификации о доставке). За подробностями можно обратиться к архитектору Марку Ричардсу Microservices vs. Service Based Architecture.

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

  • Увеличение сложности системы: будет сложнее деплоить, мониторить, поддерживать и тестировать.

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

  • Увеличение Latency: вместо вызова функции будет поход по сети.

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

  • Микросервисы усложняют поддержание целостности данных: вместо одной транзакции в базу — SAGA, TCC, XA, Workflow, Outbox pattern, и т.п.

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

Критикуя микросервисную архитектуру, я не отказываюсь от гибридных подходов, развивающихся из Modulith-архитектуры, когда мы разделяем код по модулям по доменным областям. Подобная архитектура, не имея перечисленных выше проблем, позволит получить некоторые плюсы микросервисов, вроде параллельной работы над разными модулями и быстрой сборки проекта. Ничего не помешает с течением времени, при необходимости, выделить модуль в сервис или микросервис. Кроме того, до выделения микросервисов у нас будет больше времени, чтобы «нащупать» границы.

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

Запрещаем рефакторинг обилием Unit-тестов

Тесты в умелых руках — смертельное оружие.

Авторитеты (Майк Кон, книга «Succeeding with Agile») говорят, что правильная пирамида тестирования — это 70% unit-тестов, 20% интеграционных и 10% UI. Возможно, Майк один из вдохновителей данной статьи, но что-то подобное пропагандировал и Мартин Фаулер, которого я выше цитировал. Ох, неловко вышло.

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

Проблема в том, что Unit-тесты имеют очень низкий КПД (в сравнении с интеграционными тестами), при этом значительно усложняют рефакторинг.

Представьте, у вас есть http-endpoint (далее «ручка») /link.

Ручку можно рассмотреть как черный ящик, на вход которому идут две вещи:

  • явно: запрос пользователя;

  • неявно: текущий стейт системы (базы данных, кешей).

А на выходе:

  • явно: ответ пользователю;

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

Внутри ручки /link могут использоваться 50 функций. Какие-то из них могут содержать сложную логику, и на них можно написать unit-тесты. Но я уверен, что большая частью функций не так уж и сложны.

Для демонстрации идеи давайте посмотрим на крайности.

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

Но представьте, если на все имеющиеся 50 функций написаны unit-тесты. Рефакторинг будет невозможен. Когда unit-тестами покрыто всё, придется переписывать еще и каждый тест. Уже звучит как муторная рутина. Удаление теста — всегда подозрительно на ревью, и к этому с большой вероятностью придерутся.

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

Каким же должно быть соотношение тестов? На мой взгляд, ни один адекватный программист не будет давать такие соотношения, надо смотреть на контекст. Вполне можно жить без unit-тестов вообще.

Интеграционные тесты дают свободу, юнит-тесты — душат. — Неизвестный мученик, павший во время рефакторинга.

На велосипеде по зоопарку

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

Clojure — язык свободы, в том числе свободы для саботажа, поэтому многие ярые сторонники техно-анархизма приходят за идеями и примерами решений, чтобы потом насаждать практики в другие проекты. Пишут книги, распространяя идеи на ЯП вроде Java, Go, C#, где они не применимы.

Мне довелось поработать на Clojure в австралийском стартапе, и признаюсь, что после этого проекта я наконец-то понял, чем хорош Go.

Вот что приносило боль на работе:

  • Злоупотребление примитивами теории категорий.

  • Каждая интеграция с партнерами написана в своем стиле, с разными подходами к валидации, документации, описанию запросов, тестированию.

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

  • Десятки способов работы с concurrency. Прибавьте к concurrency-примитивам Java примитивы Clojure, core async и библиотеку, имитирующую эрленговский otplike.

  • Набор огромных макросов, дающий близкую к GraphQL функциональность, с неявными ограничениями в неявных местах.

  • Редактор кода с LSP не поддерживал подсветку синтаксиса макросов на проекте. Потратив два дня, так и не смог завести «кастомную» логику статического анализа для каждого макроса через популярную clj-kondo.

  • Никакой унификации стиля и организации кода. Где-то строчки по 400 символов в длину, где-то код тестов написан прямо в файлах с бизнес-логикой (даже не в comment-форме).

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

Выбор языка программирования

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

Как бы я ни любил Clojure, не хочу позволять коллегам отнимать моё время. Когда каждый день находишь новый способ реализации цикла или разбираешься с кодом, содержащим monoid и reducer, которые оказываются простым фильтром или циклом — это начинает надоедать.

Вот забавная картинка, которая не подразумевалась, как забавная картинка:

Это только типы, есть еще мультиметоды и интерфейсы: defprotocol, gen-interface. Такое обилие возможностей отвлекает от решения реальных задач. Например, я хочу добавить валидацию через Spec и пишу ее как для map или defrecord. Но если кто-то уже описал типы через deftype для оптимизации (или gen-class, proxy, reify для совместимости с java), придется это отдельно поддерживать.

Всё описанное выше является примером проблемы «проклятья лиспа». Хотите верьте, хотите нет, но оккультное братство наложило проклятье на целое семейство языков.

The power of Lisp is its own worst enemy. — Rudolf Winestock.

С Kotlin, например, возможностей меньше, а значит, и саботаж устраивать сложнее, но ситуация не идеальная. В предыдущей статье уже обсуждалась «маскировка проблем производительности через циклы», но на Kotlin можно десятком способов реализовать и другие идеи, не только циклы.

Java кажется более предпочтительным вариантом, так как менее экспрессивна, чем Kotlin, но огромный минус языка в том, что у адептов обсуждаемой организации было очень много времени, чтобы пристреляться. Сделано всё, чтобы проекты собирались долго (аннотации и кодогенерация) и реальная логика терялась в тумане абстракций (аннотации, OOP, SOLID, DRY):

Пример из статьи «Annotations nightmare»:

@XmlElementWrapper(name="orders")
@XmlJavaTypeAdapter(OrderJaxbAdapter.class)
@XmlElements({
   @XmlElement(name="order_2",type=Order2.class),
   @XmlElement(name="old_order",type=OldOrder.class)
})
@JsonIgnore
@JsonProperty
@NotNull
@ManyToMany
@Fetch(FetchMode.SUBSELECT)
@JoinTable(
    name = "customer_order",
    joinColumns = {
        @JoinColumn(name = "customer_id", referencedColumnName = "id")
    },
    inverseJoinColumns = {
        @JoinColumn(name = "order_id", referencedColumnName = "id")
    }
)
private List orders;

Согласен, пример старенький. Тут еще не хватает аннотаций AspectJ и Dagger, так бы получилось добавить еще 4 штуки.

Подробное обсуждение языков программирования выходит за рамки статьи, оставлю только последнюю идею, прежде чем двигаться дальше. Существует книга «Java Concurrency in Practice», но нет книги «Go Concurrency in Practice».

В Go всегда пишешь type, когда нужно объявить тип, for — когда нужен цикл. Что замечательно, компилятор просто не будет работать, если используется нестандартное форматирование. А это значит, что куча бесполезной мишуры пропадет, так и не появившись:

  • Настройки, которые нужно устанавливать, приходя на новый проект;

  • Утилиты вроде ktfmt, js-beautify и тому подобные;

  • Споры о том, какое форматирование использовать на проекте;

  • «Кожаный» линтинг на code review: «поставь/убери, пожалуйста, запятую/пробел».

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

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

Если проект мой личный, всё ещё хотелось бы писать на Clojure, используя REPL (моментальный фидбек) и отличную абстракцию над sequence. По моему опыту* на Clojure намного быстрее получаешь готовое решение.

* Опыт

Больше всего работал на Kotlin и Clojure, но хотя бы более трех месяцев (по 2+ часа в день) писал на следующих языках: Java, Scheme, Fennel (Lua), Dart, Go, JavaScript, TypeScript, Python.
Назвал бы еще Bash и Groovy, но использовал их скорее бессознательно.

На практике главное, чтобы язык был адекватен задаче. Если задача — написать Launcher для Android, то ни Go, ни Clojure не подойдут, а Kotlin будет отличным выбором. Game development уровня AAA — C++. Что-то возле ML — Python.

Распознать адепта уничтожения корпораций можно в момент, когда для мобильного приложения предлагается Scala или OCaml.

Заключительные слова

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

Допускаю false positive: что-то из перечисленного выше могло попасть в моё поле зрения исключительно из-за неудачного опыта применения технологии. Прошу не обижаться и приходить дискутировать в комментарии.

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


  1. yverbin
    01.02.2025 14:40

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


  1. Cerberuser
    01.02.2025 14:40

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


    1. arturdumchev Автор
      01.02.2025 14:40

      Интеграционными тестами можно и корнеркейсы проверять на котнракт API.

      У нас на проекте сейчас примерно так выглядит (упростил синтаксис). Юнит-тестов на проекте практически нет.

      @before("path/to/before")
      @after("path/to/after")
      fun `test /link`() {
        invokeRoute("/link", params, headers, OK)
        checkParterRouteInvokacaions(1)
      }
      
      @before("path/to/before")
      @after("path/to/400_after")
      fun `test /link with bad params`() {
        invokeRoute("/link", paramsWithNoUUID, headers, BAD_REQUEST)
        invokeRoute("/link", paramsWithNoDevice, headers, BAD_REQUEST)
        checkParterRouteInvokacaions(0)
      }
      
      @before("path/to/before")
      @after("path/to/408_after")
      fun `test /link when duplicated`() {
        invokeRoute("/link", duplicatedParams, headers, ALREADY_REPORTED)
        checkParterRouteInvokacaions(0)
      }


  1. accsentive
    01.02.2025 14:40

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

    Самые яркие квазары формата Linux или Docker запущены анархистами и бунтарями, которые только после попадания в чисто бизнесовую синекуру начинали генерить Bootcamp-манифесты и KISS/DRY/etc принципы. Тот же Agile вместо ускорения и гибкости плотно взял немало команд на блесну kangoo-разработки, конвейеров а/б-тестов, при отсутствии покрытия документацией.

    Архитектурные требования определяются задачами и "болями" ключевых пользователей. Тиктоку не нужен TTM или Экселю - RPM. Но им в сто раз важнее понимать какой ключевой набор метрик является определяющим для своих 100-милионноых аудиторий.

    Знай своего клиента, будь быстрее рынка, пиши код б..д, действительно чистый и безопасный - вот этого не хватило, возможно. А с общим вектором согласен, бесконечные goto, return, "все везде" в коде, слоняния от теннисного стола к мини-футболу с вялотекущими воспоминаниями о том, что раньше кофе был "рафтее", а бананы в кают-компании больше. При этом постоянные утечки, откровенно пустые дейлики, стэк-солянки из PHP/Ruby, C#, Delphi, JS, Python, PG/MySQL, MongoDB, Airflow, Kafka+RabbitM, раскиданных по дюжине VDS - вот такие кланы не вчера и массово-успешно формируют FAANG's schools. Жаль.

    ps. кстати, если в слово Google добавить тире, то дословным переводом станет "гуляй-глазей" ))


    1. arturdumchev Автор
      01.02.2025 14:40

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

      Кажется, что вот эта часть не про архитектуру, а про фичи, которые нужно реализовать.

      Тиктоку не нужен TTM или Экселю - RPM.

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

      А аналогом SLA для Excel будет не RPM, а crashfree, например.


  1. kenomimi
    01.02.2025 14:40

    Любые тесты мешают работать, это факт. И переписывать их - мало удовольствия. Но тесты нужны, когда две трети команды - мартышки с гранатой - могут закоммитить что угодно. Я лично видел закоммиченый йух ascii-артом в шаблоне странички, коммит только из пробелов (чёрт потом признался, что набивал количество коммитов, зачем - хз, KPI нет вообще никаких), коммит, когда чел удалил часть бизнес-логики, потому что "там какая-то непонятная фигня", и так далее. Ревьювить на 100% на активном проекте практически нереально, дерьмо всё равно проскочит мимо ревьювера... Зато если ты коммитом сломал тесты - ты закоммитил кал, и точка. Если твой коммит не прошел кодстайл-чек - снова бери и приводи свой код в порядок. Сонар ругнулся - иди и исправляй. 80% косяков отлавливаются автоматизированными проверками, серьезно разгружая ревьювера. Да, это время CI\CD, но время машины тут дешевле времени кожаного ревьювера, потому ок. Гнать мартышек нельзя - ибо нагружать рутиной немногочисленных крутых спецов очень плохая идея, от этого сильно отрастает TTM сложных фич, которые они пилят.

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

    Аспекты. Крутая штука для логгирования и трекинга вызовов в отладочных сборках. Не менее круто аспектами собирать тонкую статистику нагрузочного тестирования. А вот если у вас аспект влияет на основной код хоть как-то - вы ССЗБ, не надо забивать болты молотком в гайки.

    Кодогенерация везде. Зловещий артефакт времен Древних, которые писали в vim 0.0.1 на разрешении 80х22 на оранжевом плазменном дисплее лампового же вычислительного устройства. Сейчас кодогенерация встроена в приличную IDE, а еще есть чатгпт, который уж что-то, а типовые шаблоны генерит идеально. Но адепты Культа Древних продолжают совать везде тот же ломбок...


    1. arturdumchev Автор
      01.02.2025 14:40

      Спасибо за комментарий, со многим согласен.

      Про тесты я тоже считаю, что нужны, просто я чаще пишу интеграционные тесты. Раздражает, когда пытаются тестировать каждую функцию просто для галочки или для покрытия. Хотя тот же сонар видит покрытие и по интеграционным тестам — куда заходил код во время выполнения.

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

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

      Вменяемой альтернативы им просто не вижу.

      А вы не писали бекенд на Go? Там подход такой, чтобы контролировать, что проиходит, «без магии», как любят говорить. Сейчас поддерживаю бекенд на Kotlin + Ktor, тоже без магии живем. И до этого на Clojure был бекенд — там такой же подход (но проблемы тоже были, в статье как раз описал).


      1. kenomimi
        01.02.2025 14:40

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

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


        1. arturdumchev Автор
          01.02.2025 14:40

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

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

          Еще попадался доклад, где меинтейнер Clojure-плагина для Идеи переписывал его с Clojure на Kotlin с помощью ИИ. Что я из этого доклада подметил — не надо обучать/дообучать на своей кодовой базе. Все что нужно — большой промпт с примерами, как надо. Редакторы, вроде тех, что скинул, и плагинов, что у разрабтчиков идеи внутри есть уже позволяют брать файлы из контекста и добавлять их в промпт.


  1. Lewigh
    01.02.2025 14:40

    В целом со статьей согласен и со всей этой болью. Но

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

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

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

    К сожалению не все так просто. Это скорей самообман сознания желающего найти простоту радикальным образом.
    Нельзя просто так взять и повыкидывать все из ЯП.

    Возьмем тот же Kotlin, который излишне экспрессивный и Java, философия которой изначально строилась в стиле Go - повыкидывать все из языка, все ограничить и запретить.
    Что мы имеем в Java?
    Отсутствие перегрузки операторов приводит к огромной лапше в коде в местах где это бы пригодилось бы. Отсутствие именованных параметров - к повсеместному паттену builder.
    А главное, отсутствие выразительности языка привело к обилию паттернов и самое ужасное куче костылей от Lombok до взлета популярности проклятого AOP, который превратил Java из, в теории простого и читабельного языка в какое то неявное адище, в проектах с AOP.
    Если в языке не хватает выразительности чтобы решить проблему лаконично то часто это будет приводить либо к огромным портянках кода для простых вещей, либо разработчики быстро придумаю как прикрутить туда какую-нибудь вундервафлю и может стать еще хуже.

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

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

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


    1. accsentive
      01.02.2025 14:40

      Все так, при том что в wikipedia алфавитный список ЯП уже содержит 350+ записей. И горшочек продолжает варить ))


      1. kenomimi
        01.02.2025 14:40

        Эволюция это хорошо. А то, что где-то возникает очередной утконос - ну штош, никто не обещал 100% успех.


    1. arturdumchev Автор
      01.02.2025 14:40

      проблема что забыли выкинуть грабли на которые тут и там можно наступить

      Вы про то, что надо знать детали реализации slice, чтобы им пользоваться? В остальном вроде всё терпимо.

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

      Возможно, будет лучше. На многие грабли уже наступили. В ООП как в панацею мало кто верит, concurrency сразу спроектировали простым, комьюнити ценит ограничения и простоту — читаю Golang сабреддит, общее направление ощущается строгим. Например, не рекомендуют DRY злоупотреблять. Вновь пришедшие из Java ценят, что в отличие от спринга магии нет. Негодавали даже из-за появления дженериков — аля в библиотеках используйте, пожалуйста, но не надо в бизнес-логику тащить.

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


      1. Lewigh
        01.02.2025 14:40

        Вы про то, что надо знать детали реализации slice, чтобы им пользоваться? В остальном вроде всё терпимо.

        Ну да к примеру неочевидность slices. Далее, на вскидку, неочевидность работы с памятью. К примеру повергает в недоумение, когда при возврате из функции указателя на структуру происходит неявный боксинг, что по моему мнению контринтуитивно.
        То что при всей претенциозности обработки ошибок в Go - их можно просто не обработать или сделать это неправильно малозаметным способом.
        Работа со строками которые под капотом вроде бы должны быть UTF-8 но нет, там может быть все что угодно. len от строки выдаст количество байт а не символов.
        Дикие вещи вроде возврата из функции по имени.
        nil указатель, который рассыпает ворох граблей тут и там.
        Ну и т.д.

        concurrency сразу спроектировали простым

        Не простым а минималистичным. Нельзя сделать сложную задачу простой. Мы либо усложняем язык и делаем более сложные но более безопасные решения(привет Rust) либо заметаем сложность под ковер как в Go а потом наступаем. Сложность будет либо в изучении языка либо в решении проблемы. К тому же минимализм, при всей его привлекательности приносит также много проблем. К примеру в Java большая хорошая библиотека для многопоточки, где почти на все случаи жизни что-то имеется, да больше да сложнее и разнообразнее, но под конкретную проблемы мы просто возьмем подходящий проверенный инструмент. В случае Go это приведет либо к костылям либо к чужим либам. Это как предложить хирургу, в арсенале которого сотня инструментов, пользоваться одним типом скальпеля для любой операции. В плане обучения, единообразия, поставок инструментов вроде ок, но в плане результата - такое себе.

        В ООП как в панацею мало кто верит

        Хороший пример Rust. Язык имеющий определенный сходства с Go - отказ от ООП, наследования типов, отказ от исключений и т.д. Но сразу нашлись умники которые пошли писать библиотеки добавляющие исключения, наследование и прочее в язык в котором идиоматически этого быть не должно. Более того, нашлись гении которые пошли писать для него сборщик мусора. Так что к сожалению пока это все базируется на идеологии, а потом придет модный спикер и расскажет чего в Go не хватает.

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


  1. moonster
    01.02.2025 14:40

    Хочется вступиться за юнит-тесты.

    Хороший тест - это еще и документация.

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

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


    1. arturdumchev Автор
      01.02.2025 14:40

      От юнит-тестов не предлагаю отказываться, вот как раз такой подход выглядит хорошо:

      Последний реальный пример - уменьшение количества тестов с 118 до 9.

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

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

      А вот были бы тест на контракт (рест эндпоинта, интерфейс, модуль), можно внутри сколь угодно менять модель.

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


      1. moonster
        01.02.2025 14:40

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

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

        А вот были бы тест на контракт (рест эндпоинта, интерфейс, модуль), можно внутри сколь угодно менять модель.

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

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

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

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


  1. Madfisht3
    01.02.2025 14:40

    Согласен, но не со всем.

    Есть поговорка, которая кратко характеризует описываемые феномены -"Заставь дурака молиться, он и лоб себе расшибëт."

    Перед тем как внедрять модную технологию или методологию, нужно задаться вопросом -"Зачем? Без этого уже нельзя?"


  1. Dhwtj
    01.02.2025 14:40

    Давайте сразу разделим понятия микросервисы и сервисы (service-based architecture). Сервисы — это разбитый по доменным областям монолит (например, сервис доставки). Микросервисы — уже разбитые по функциям сервисы (например, микросервис нотификации о доставке). За подробностями можно обратиться к архитектору Марку Ричардсу Microservices vs. Service Based Architecture.

    Не надо путать причину и следствие. Пусть, даже сам Марк это путает.

    Микросервисы MSA это тот же SBA но с более жёсткими требованиями к независимости сервисов, главное из которых отсутствие связей по БД, то есть использование своих БД у каждого сервиса.

    SBA не бывает монолитом. Для такого монолита есть своё название: modular monolith.

    Внёс порядок в термины. Статья весьма интересная.

    По заготовке: не разработчики, а архитекторы. И причём тут заговор? Заговор это осознанное действие.

    Лучше так:

    "Как архитекторы вредят корпорациям"


    1. arturdumchev Автор
      01.02.2025 14:40

      "Как архитекторы вредят корпорациям"

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

      Я же ещё в статье про принципы пишу, а они все про решения разработчиков.

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

      И причём тут заговор? Заговор это осознанное действие

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


  1. touols
    01.02.2025 14:40

    Заговор програмистов анархистов - это что-то новенькое :-). Но вообще

    Миром правит не тайная ложа, а явная лажа

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


    1. arturdumchev Автор
      01.02.2025 14:40

      На всякий случай уточню, что это просто форма подачи, я не считаю, что организация существует ;)


      1. touols
        01.02.2025 14:40

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

        И покороче. Но это на любителя уже.


        1. Ivan22
          01.02.2025 14:40

          тогда будет не интересно, а хабр всетаки развлекательный ресурс


          1. touols
            01.02.2025 14:40

            Разве? Хабр это как научпоп. И развлекательно и серьезно. Сказки мне здесь например не интересны.


  1. touols
    01.02.2025 14:40

    Что-то весьма интересно, а что-то для кустаря-одиночки сложно. Тут изобретаю модульность для большого проекта. И микросервисы для меня не понятно что это такое. В общем-то с задачей разобрался и разберусь, но несколько костыльно. Хотелось бы понимать как лучше сделать. Сейчас такое впечатление, что микросервисы это модная идея и сильно увлекаться ей вредно. Но модульность нужна, чтобы вообще не потеряться в куче кода. Разбивка на классы, компоненты(плагины) и т.д. Можно сделать примеры разбивки кода на модули и на (микро)сервисы? Что почему и как удобнее сделать?


    1. arturdumchev Автор
      01.02.2025 14:40

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

      Простых ответов нет, а примеров в интернете много. В популярном open source обычно хороший код.


      1. touols
        01.02.2025 14:40

        ERP система для производства с интегрированным интернет-магазином. Это описание проекта. Стек MODX, php, vue и самописное API с табличным редактором на основе PrimeVue https://github.com/touol/gtsAPI. Это вкратце. Для полного описания нужно кучу статей писать. Что сейчас не реально. Но я могу что-то написать, если пойму что будет кому-то интересно. Точнее даже, через месяца 3 что-то точно напишу, но для сообщества MODX. А писать ли на Хабре не знаю...


      1. touols
        01.02.2025 14:40

        Простых ответов нет, а примеров в интернете много. В популярном open source обычно хороший код.

        Разобранных примеров: "Что? Почему и как?" я как-то не встречал. Может плохо искал??


  1. DenSigma
    01.02.2025 14:40

    Добавлю по языкам.

    Можно в Java писать в стиле Clojure. Максимально функционально. Максимально Streams, когда они нафиг не нужны - циклы, условия, обработка null. Можно наворачивать вызовы функций друг в друга, крайне желательно с лямбдами. При этом используя стохастическое форматирование. Огромные методы без единой переменной. Создается адовая матрешка, кочан вызовов, на разбор которого можно тратить целые часы. Что вызывается, в каком порядке? Какие данные передаются в вызовы, что возвращается? Невозможно понять ни чтением, ни дебаггингом, потому что идея при включении показа возвращаемого значения начинает тупить, а по f7 внутрь лямбд она не заходит.

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


    1. arturdumchev Автор
      01.02.2025 14:40

      Согласен. Недавно коллега рассказывал, что у них запрещено стримы использовать на Java, обычный for везде. Везунчик, что оказался в такой команде.